React + TypeScript en 2026 : Guide Complet du Développeur Modern

Meta description : Maîtrisez React avec TypeScript en 2026 : hooks typés, patterns avancés, performance, tests. Guide complet pour développeurs qui veulent écrire du code React robuste et maintenable. Mots-clés cibles : react typescript, react ts guide, typescript react 2026, hooks typescript, développement react moderne, frontend typescript Slug : react-typescript-2026-guide-complet

React + TypeScript : Le Duo Incontournable en 2026

En 2026, 87% des nouveaux projets React utilisent TypeScript. Et ce n'est pas un hasard : le typage statique élimine des classes entières de bugs, améliore l'autocomplétion, et rend le code plus lisible pour toute l'équipe.

Ce guide vous montre comment exploiter React + TypeScript efficacement — pas juste les bases, mais les patterns qui font la différence en production.


1. Setup Moderne en 2 Minutes

Vite (Recommandé en 2026)

npm create vite@latest mon-app -- --template react-ts

cd mon-app

npm install

npm run dev

Next.js (Si vous avez besoin de SSR)

npx create-next-app@latest mon-app --typescript --app --tailwind

Structure de Projet

src/

├── components/

│ ├── ui/ # Composants réutilisables

│ ├── features/ # Composants métier

│ └── layouts/ # Layouts de page

├── hooks/ # Custom hooks

├── lib/ # Utilitaires

├── types/ # Types partagés

├── services/ # Appels API

└── stores/ # State management


2. Typer vos Composants

Props avec Interface

interface UserCardProps {

user: {

id: string

name: string

email: string

avatar?: string // optionnel

}

onEdit: (id: string) => void

variant?: 'compact' | 'full' // union type

}

function UserCard({ user, onEdit, variant = 'full' }: UserCardProps) {

return (

<div className={card card-${variant}}>

<h3>{user.name}</h3>

{variant === 'full' && <p>{user.email}</p>}

<button onClick={() => onEdit(user.id)}>Éditer</button>

</div>

)

}

Children Typés

// Children simple (JSX)

interface LayoutProps {

children: React.ReactNode

sidebar?: React.ReactNode

}

// Render props

interface DataListProps<T> {

items: T[]

renderItem: (item: T, index: number) => React.ReactNode

keyExtractor: (item: T) => string

}

function DataList<T>({ items, renderItem, keyExtractor }: DataListProps<T>) {

return (

<ul>

{items.map((item, i) => (

<li key={keyExtractor(item)}>{renderItem(item, i)}</li>

))}

</ul>

)

}

// Utilisation

<DataList

items={users}

renderItem={(user) => <span>{user.name}</span>}

keyExtractor={(user) => user.id}

/>

Composants Polymorphiques

type ButtonProps<T extends React.ElementType = 'button'> = {

as?: T

variant: 'primary' | 'secondary' | 'ghost'

size?: 'sm' | 'md' | 'lg'

} & React.ComponentPropsWithoutRef<T>

function Button<T extends React.ElementType = 'button'>({

as,

variant,

size = 'md',

className,

...props

}: ButtonProps<T>) {

const Component = as || 'button'

return (

<Component

className={btn btn-${variant} btn-${size} ${className ?? ''}}

{...props}

/>

)

}

// Utilisation

<Button variant="primary">Cliquer</Button>

<Button as="a" variant="secondary" href="/about">À propos</Button>


3. Hooks Typés

useState

// Type inféré

const [count, setCount] = useState(0) // number

// Type explicite (nécessaire pour les objets)

interface User {

id: string

name: string

email: string

}

const [user, setUser] = useState<User | null>(null)

// Avec un état initial

const [filters, setFilters] = useState<{

search: string

category: string

page: number

}>({

search: '',

category: 'all',

page: 1

})

useRef

// Ref DOM

const inputRef = useRef<HTMLInputElement>(null)

// Ref mutable (valeur qui persiste sans re-render)

const intervalRef = useRef<ReturnType<typeof setInterval>>(null)

function handleFocus() {

inputRef.current?.focus() // optional chaining

}

useReducer

type Action =

| { type: 'increment' }

| { type: 'decrement' }

| { type: 'reset'; payload: number }

| { type: 'set'; payload: number }

interface State {

count: number

history: number[]

}

function reducer(state: State, action: Action): State {

switch (action.type) {

case 'increment':

return {

count: state.count + 1,

history: [...state.history, state.count + 1]

}

case 'decrement':

return {

count: state.count - 1,

history: [...state.history, state.count - 1]

}

case 'reset':

return { count: action.payload, history: [] }

case 'set':

return {

count: action.payload,

history: [...state.history, action.payload]

}

}

}

const [state, dispatch] = useReducer(reducer, { count: 0, history: [] })

dispatch({ type: 'set', payload: 42 })

Custom Hooks

// Hook de fetch générique

function useFetch<T>(url: string) {

const [data, setData] = useState<T | null>(null)

const [error, setError] = useState<Error | null>(null)

const [loading, setLoading] = useState(true)

useEffect(() => {

const controller = new AbortController()

async function fetchData() {

try {

setLoading(true)

const res = await fetch(url, { signal: controller.signal })

if (!res.ok) throw new Error(HTTP ${res.status})

const json = await res.json() as T

setData(json)

} catch (err) {

if (err instanceof Error && err.name !== 'AbortError') {

setError(err)

}

} finally {

setLoading(false)

}

}

fetchData()

return () => controller.abort()

}, [url])

return { data, error, loading }

}

// Utilisation

const { data: users, loading } = useFetch<User[]>('/api/users')


4. Context API Typé

interface AuthContext {

user: User | null

login: (email: string, password: string) => Promise<void>

logout: () => void

isLoading: boolean

}

const AuthContext = createContext<AuthContext | null>(null)

function useAuth() {

const context = useContext(AuthContext)

if (!context) {

throw new Error('useAuth must be used within AuthProvider')

}

return context

}

function AuthProvider({ children }: { children: React.ReactNode }) {

const [user, setUser] = useState<User | null>(null)

const [isLoading, setIsLoading] = useState(true)

const login = async (email: string, password: string) => {

const res = await fetch('/api/auth/login', {

method: 'POST',

body: JSON.stringify({ email, password }),

headers: { 'Content-Type': 'application/json' }

})

const data = await res.json() as User

setUser(data)

}

const logout = () => setUser(null)

return (

<AuthContext.Provider value={{ user, login, logout, isLoading }}>

{children}

</AuthContext.Provider>

)

}


5. Formulaires Typés

React Hook Form + Zod (La Meilleure Combo)

import { useForm } from 'react-hook-form'

import { zodResolver } from '@hookform/resolvers/zod'

import { z } from 'zod'

const contactSchema = z.object({

name: z.string().min(2, 'Le nom doit faire au moins 2 caractères'),

email: z.string().email('Email invalide'),

message: z.string().min(10, 'Le message doit faire au moins 10 caractères'),

category: z.enum(['support', 'sales', 'partnership']),

newsletter: z.boolean().default(false)

})

type ContactForm = z.infer<typeof contactSchema>

function ContactPage() {

const {

register,

handleSubmit,

formState: { errors, isSubmitting }

} = useForm<ContactForm>({

resolver: zodResolver(contactSchema)

})

const onSubmit = async (data: ContactForm) => {

await fetch('/api/contact', {

method: 'POST',

body: JSON.stringify(data),

headers: { 'Content-Type': 'application/json' }

})

}

return (

<form onSubmit={handleSubmit(onSubmit)}>

<input {...register('name')} placeholder="Nom" />

{errors.name && <span>{errors.name.message}</span>}

<input {...register('email')} placeholder="Email" type="email" />

{errors.email && <span>{errors.email.message}</span>}

<select {...register('category')}>

<option value="support">Support</option>

<option value="sales">Commercial</option>

<option value="partnership">Partenariat</option>

</select>

<textarea {...register('message')} placeholder="Votre message" />

{errors.message && <span>{errors.message.message}</span>}

<label>

<input {...register('newsletter')} type="checkbox" />

Recevoir la newsletter

</label>

<button type="submit" disabled={isSubmitting}>

{isSubmitting ? 'Envoi...' : 'Envoyer'}

</button>

</form>

)

}


6. Appels API Typés

Avec TanStack Query (React Query)

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

interface Product {

id: string

name: string

price: number

category: string

}

// Service API typé

const productApi = {

getAll: async (): Promise<Product[]> => {

const res = await fetch('/api/products')

if (!res.ok) throw new Error('Failed to fetch products')

return res.json()

},

getById: async (id: string): Promise<Product> => {

const res = await fetch(/api/products/${id})

if (!res.ok) throw new Error('Product not found')

return res.json()

},

create: async (data: Omit<Product, 'id'>): Promise<Product> => {

const res = await fetch('/api/products', {

method: 'POST',

body: JSON.stringify(data),

headers: { 'Content-Type': 'application/json' }

})

return res.json()

}

}

// Dans le composant

function ProductList() {

const { data: products, isLoading, error } = useQuery({

queryKey: ['products'],

queryFn: productApi.getAll

})

const queryClient = useQueryClient()

const createMutation = useMutation({

mutationFn: productApi.create,

onSuccess: () => {

queryClient.invalidateQueries({ queryKey: ['products'] })

}

})

if (isLoading) return <p>Chargement...</p>

if (error) return <p>Erreur: {error.message}</p>

return (

<ul>

{products?.map(product => (

<li key={product.id}>

{product.name} — {product.price}€

</li>

))}

</ul>

)

}


7. Performance avec TypeScript

React.memo Typé

interface ExpensiveListProps {

items: Product[]

onSelect: (id: string) => void

}

const ExpensiveList = React.memo<ExpensiveListProps>(

function ExpensiveList({ items, onSelect }) {

return (

<ul>

{items.map(item => (

<li key={item.id} onClick={() => onSelect(item.id)}>

{item.name}

</li>

))}

</ul>

)

}

)

useCallback et useMemo Typés

const filteredProducts = useMemo<Product[]>(

() => products.filter(p => p.category === selectedCategory),

[products, selectedCategory]

)

const handleSelect = useCallback(

(id: string) => {

setSelectedId(id)

onProductSelect?.(id)

},

[onProductSelect]

)


8. Tests avec TypeScript

Testing Library

import { render, screen, fireEvent, waitFor } from '@testing-library/react'

import userEvent from '@testing-library/user-event'

describe('UserCard', () => {

const mockUser: User = {

id: '1',

name: 'John Doe',

email: 'john@example.com'

}

it('renders user info', () => {

const onEdit = vi.fn()

render(<UserCard user={mockUser} onEdit={onEdit} />)

expect(screen.getByText('John Doe')).toBeInTheDocument()

expect(screen.getByText('john@example.com')).toBeInTheDocument()

})

it('calls onEdit with user id', async () => {

const onEdit = vi.fn()

const user = userEvent.setup()

render(<UserCard user={mockUser} onEdit={onEdit} />)

await user.click(screen.getByText('Éditer'))

expect(onEdit).toHaveBeenCalledWith('1')

})

it('hides email in compact variant', () => {

const onEdit = vi.fn()

render(<UserCard user={mockUser} onEdit={onEdit} variant="compact" />)

expect(screen.queryByText('john@example.com')).not.toBeInTheDocument()

})

})


9. Patterns Avancés

Discriminated Unions pour les États

type AsyncState<T> =

| { status: 'idle' }

| { status: 'loading' }

| { status: 'success'; data: T }

| { status: 'error'; error: Error }

function useAsyncData<T>(fetcher: () => Promise<T>) {

const [state, setState] = useState<AsyncState<T>>({ status: 'idle' })

const execute = async () => {

setState({ status: 'loading' })

try {

const data = await fetcher()

setState({ status: 'success', data })

} catch (err) {

setState({ status: 'error', error: err as Error })

}

}

return { ...state, execute }

}

// Utilisation — TypeScript sait quel champ existe selon le status

function ProductPage() {

const state = useAsyncData(() => productApi.getAll())

switch (state.status) {

case 'idle':

return <button onClick={state.execute}>Charger</button>

case 'loading':

return <Spinner />

case 'error':

return <Alert>{state.error.message}</Alert>

case 'success':

return <ProductList items={state.data} />

}

}

Generic Components

interface TableProps<T> {

data: T[]

columns: {

key: keyof T

header: string

render?: (value: T[keyof T], row: T) => React.ReactNode

}[]

onRowClick?: (row: T) => void

}

function Table<T extends { id: string }>({

data,

columns,

onRowClick

}: TableProps<T>) {

return (

<table>

<thead>

<tr>

{columns.map(col => (

<th key={String(col.key)}>{col.header}</th>

))}

</tr>

</thead>

<tbody>

{data.map(row => (

<tr key={row.id} onClick={() => onRowClick?.(row)}>

{columns.map(col => (

<td key={String(col.key)}>

{col.render

? col.render(row[col.key], row)

: String(row[col.key])}

</td>

))}

</tr>

))}

</tbody>

</table>

)

}


10. Configuration TypeScript Optimale

tsconfig.json

{

"compilerOptions": {

"target": "ES2022",

"lib": ["ES2023", "DOM", "DOM.Iterable"],

"module": "ESNext",

"moduleResolution": "bundler",

"jsx": "react-jsx",

"strict": true,

"noUnusedLocals": true,

"noUnusedParameters": true,

"noFallthroughCasesInSwitch": true,

"noUncheckedIndexedAccess": true,

"exactOptionalPropertyTypes": true,

"paths": {

"@/": ["./src/"]

}

},

"include": ["src"]

}


Conclusion

React + TypeScript n'est plus optionnel en 2026 — c'est la norme. Le typage statique vous fait gagner du temps sur le long terme : moins de bugs en production, meilleure documentation vivante, et refactoring sans peur.

Pour démarrer rapidement :

> Besoin d'un projet React + TypeScript prêt pour la production ? Notre SaaS Boilerplate inclut authentification, paiements Stripe, dashboard admin — le tout entièrement typé et testé.


FAQ

TypeScript ralentit-il le développement ?

Au début oui (courbe d'apprentissage). Après 2-3 semaines, il accélère le développement grâce à l'autocomplétion et la détection d'erreurs instantanée.

Faut-il typer tout ?

Non. Laissez TypeScript inférer quand c'est clair (variables locales, retours de fonctions simples). Typez explicitement les interfaces publiques (props, API responses, hooks exports).

React ou Next.js avec TypeScript ?

React (Vite) pour les SPA et les dashboards. Next.js pour les sites qui ont besoin de SEO (SSR/SSG) ou de server-side logic.

Quel est le meilleur state manager typé ?

Zustand pour la simplicité, Jotai pour les atomes, TanStack Query pour le server state. Redux Toolkit si vous avez déjà Redux.