Top 30 questions d'entretien React : guide complet pour réussir

Les 30 questions d'entretien React les plus posées en 2026. Réponses détaillées, exemples de code et conseils pour décrocher votre poste de développeur React.

Illustration de questions d'entretien React avec des composants et hooks interconnectés

Les entretiens techniques React évaluent la compréhension des concepts fondamentaux, des patterns avancés et des bonnes pratiques. Ce guide présente les 30 questions les plus fréquentes, avec des réponses détaillées et des exemples de code pour se préparer efficacement.

Conseil de préparation

Ces questions sont classées par niveau de difficulté. Commencer par maîtriser les fondamentaux avant d'aborder les concepts avancés permet une préparation plus structurée.

Fondamentaux React

1. Qu'est-ce que le Virtual DOM et pourquoi React l'utilise ?

Le Virtual DOM est une représentation JavaScript légère du DOM réel. React utilise cette abstraction pour optimiser les mises à jour de l'interface.

Le processus fonctionne en trois étapes : React crée d'abord une copie virtuelle du DOM, puis compare cette copie avec la version précédente lors des changements (algorithme de diffing), et enfin applique uniquement les modifications nécessaires au DOM réel (reconciliation).

jsx
// Exemple simplifié du concept
// Quand l'état change, React ne recrée pas tout le DOM
function Counter() {
  const [count, setCount] = useState(0)

  // Seul le span contenant count sera mis à jour dans le DOM réel
  // Le reste du composant n'est pas touché
  return (
    <div>
      <h1>Compteur</h1>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  )
}

Cette approche évite les opérations DOM coûteuses et permet des mises à jour performantes même pour des interfaces complexes.

2. Quelle est la différence entre un composant fonctionnel et un composant classe ?

Les composants fonctionnels sont des fonctions JavaScript qui reçoivent des props et retournent du JSX. Depuis React 16.8, les hooks permettent d'utiliser l'état et le cycle de vie dans les composants fonctionnels.

jsx
// Composant fonctionnel (recommandé)
// Plus concis, plus facile à tester, supporte les hooks
function Welcome({ name }) {
  const [visits, setVisits] = useState(0)

  useEffect(() => {
    setVisits(v => v + 1)
  }, [])

  return <h1>Bonjour {name}, visite #{visits}</h1>
}

// Composant classe (legacy)
// Plus verbeux, nécessite la gestion du this
class WelcomeClass extends React.Component {
  state = { visits: 0 }

  componentDidMount() {
    this.setState(prev => ({ visits: prev.visits + 1 }))
  }

  render() {
    return <h1>Bonjour {this.props.name}, visite #{this.state.visits}</h1>
  }
}

Les composants fonctionnels sont aujourd'hui le standard. Les composants classe restent supportés mais ne sont plus recommandés pour les nouveaux projets.

3. Comment fonctionne le JSX ?

JSX est une extension syntaxique de JavaScript qui permet d'écrire du markup dans le code. Ce n'est pas du HTML mais du JavaScript déguisé.

jsx
// Ce que nous écrivons (JSX)
const element = (
  <div className="container">
    <h1>Titre</h1>
    <p>Paragraphe</p>
  </div>
)

// Ce que Babel compile (JavaScript pur)
const element = React.createElement(
  'div',
  { className: 'container' },
  React.createElement('h1', null, 'Titre'),
  React.createElement('p', null, 'Paragraphe')
)

Les différences avec HTML incluent : className au lieu de class, htmlFor au lieu de for, le camelCase pour les attributs (onClick, tabIndex), et la fermeture obligatoire des balises auto-fermantes.

4. Qu'est-ce que le state et les props ?

Les props sont des données passées d'un composant parent à un enfant. Elles sont en lecture seule. Le state est l'état interne d'un composant, modifiable via des setters.

UserCard.jsxjsx
// name et role sont des props (immuables)
function UserCard({ name, role }) {
  // isExpanded est du state (mutable)
  const [isExpanded, setIsExpanded] = useState(false)

  return (
    <div className="card">
      <h2>{name}</h2>
      <p>{role}</p>

      {/* Modifier le state déclenche un re-render */}
      <button onClick={() => setIsExpanded(!isExpanded)}>
        {isExpanded ? 'Réduire' : 'Détails'}
      </button>

      {isExpanded && <UserDetails name={name} />}
    </div>
  )
}

// Utilisation
<UserCard name="Alice" role="Developer" />

La règle fondamentale : les props descendent (parent → enfant), le state est local à chaque composant.

5. Pourquoi les clés (keys) sont-elles importantes dans les listes ?

Les clés aident React à identifier quels éléments ont changé, été ajoutés ou supprimés dans une liste. Sans clés uniques et stables, React peut avoir des comportements inattendus.

jsx
// ❌ Mauvaise pratique : index comme clé
// Problème : si l'ordre change, React perd le tracking
{items.map((item, index) => (
  <ListItem key={index} item={item} />
))}

// ✅ Bonne pratique : identifiant unique et stable
{items.map(item => (
  <ListItem key={item.id} item={item} />
))}

// Exemple concret du problème avec les index
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Apprendre React' },
    { id: 2, text: 'Créer un projet' }
  ])

  // En supprimant le premier élément avec key={index}
  // React pensera que l'élément 0 a changé de contenu
  // au lieu de comprendre que l'élément a été supprimé

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

Hooks React

6. Expliquez useState et ses pièges courants

useState permet de gérer l'état local d'un composant fonctionnel. Le setter peut prendre une valeur ou une fonction de mise à jour.

jsx
// Déclaration avec valeur initiale
const [count, setCount] = useState(0)

// ❌ Piège : mises à jour multiples dans le même cycle
function increment() {
  setCount(count + 1) // count = 0, définit 1
  setCount(count + 1) // count = 0 encore, définit 1
  setCount(count + 1) // count = 0 encore, définit 1
  // Résultat final : 1 (pas 3)
}

// ✅ Solution : utiliser la fonction de mise à jour
function incrementCorrect() {
  setCount(prev => prev + 1) // 0 → 1
  setCount(prev => prev + 1) // 1 → 2
  setCount(prev => prev + 1) // 2 → 3
  // Résultat final : 3
}

// ❌ Piège : mutation d'objet
const [user, setUser] = useState({ name: 'Alice', age: 25 })
user.age = 26 // Mutation directe, pas de re-render

// ✅ Solution : créer un nouvel objet
setUser({ ...user, age: 26 })
// ou
setUser(prev => ({ ...prev, age: 26 }))

7. Comment fonctionne useEffect et son tableau de dépendances ?

useEffect permet d'exécuter des effets de bord après le rendu. Le tableau de dépendances contrôle quand l'effet s'exécute.

jsx
// Exécuté à chaque rendu (rare, à éviter généralement)
useEffect(() => {
  console.log('Rendu effectué')
})

// Exécuté uniquement au montage (équivalent componentDidMount)
useEffect(() => {
  console.log('Composant monté')

  // Cleanup au démontage (équivalent componentWillUnmount)
  return () => {
    console.log('Composant démonté')
  }
}, [])

// Exécuté quand userId change
useEffect(() => {
  async function fetchUser() {
    const response = await fetch(`/api/users/${userId}`)
    const data = await response.json()
    setUser(data)
  }

  fetchUser()
}, [userId])

// ❌ Dépendance manquante - bug subtil
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1) // count est "capturé" à sa valeur initiale
  }, 1000)
  return () => clearInterval(timer)
}, []) // count manque dans les dépendances

// ✅ Correction avec fonction de mise à jour
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1) // Pas besoin de count dans les deps
  }, 1000)
  return () => clearInterval(timer)
}, [])
Règle ESLint

Toujours activer eslint-plugin-react-hooks pour détecter les dépendances manquantes. Cette règle prévient de nombreux bugs difficiles à diagnostiquer.

8. Quand utiliser useMemo et useCallback ?

Ces hooks permettent la mémorisation pour éviter des recalculs ou re-créations inutiles. Attention à ne pas en abuser.

jsx
// useMemo : mémorise une valeur calculée
function ProductList({ products, filter }) {
  // Recalculé uniquement si products ou filter changent
  const filteredProducts = useMemo(() => {
    console.log('Filtrage en cours...')
    return products.filter(p => p.category === filter)
  }, [products, filter])

  return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}

// useCallback : mémorise une fonction
function ParentComponent() {
  const [count, setCount] = useState(0)

  // Sans useCallback, handleClick est recréé à chaque rendu
  // Causant des re-renders inutiles de ExpensiveChild
  const handleClick = useCallback((id) => {
    console.log('Clicked:', id)
  }, []) // Dépendances vides = fonction stable

  return (
    <>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      {/* React.memo sur ExpensiveChild pour que ça soit efficace */}
      <ExpensiveChild onClick={handleClick} />
    </>
  )
}

// ❌ Sur-optimisation : pas besoin ici
const SimpleComponent = () => {
  // Ce calcul est trivial, useMemo ajoute de l'overhead
  const doubled = useMemo(() => 2 * 2, [])
  return <span>{doubled}</span>
}

Utiliser ces hooks uniquement quand un problème de performance est identifié ou pour stabiliser les références passées à des composants mémoïsés.

9. Comment fonctionne useRef et ses cas d'usage ?

useRef crée une référence mutable qui persiste entre les rendus sans déclencher de re-render quand elle change.

jsx
// Cas 1 : Accéder à un élément DOM
function TextInput() {
  const inputRef = useRef(null)

  const focusInput = () => {
    inputRef.current.focus()
  }

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </>
  )
}

// Cas 2 : Stocker une valeur mutable sans re-render
function Timer() {
  const [seconds, setSeconds] = useState(0)
  const intervalRef = useRef(null)

  const start = () => {
    // Stocker l'ID de l'interval pour pouvoir l'arrêter
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1)
    }, 1000)
  }

  const stop = () => {
    clearInterval(intervalRef.current)
  }

  return (
    <div>
      <span>{seconds}s</span>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  )
}

// Cas 3 : Garder la valeur précédente
function usePrevious(value) {
  const ref = useRef()

  useEffect(() => {
    ref.current = value
  }, [value])

  return ref.current
}

10. Expliquez useContext et quand l'utiliser

useContext permet d'accéder à un contexte React sans prop drilling. Idéal pour des données globales comme le thème ou l'utilisateur connecté.

1. Créer le contextejsx
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {}
})

// 2. Créer le provider
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }, [])

  // Mémoriser la valeur pour éviter les re-renders inutiles
  const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme])

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

// 3. Utiliser le contexte
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext)

  return (
    <button
      onClick={toggleTheme}
      className={theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-gray-800'}
    >
      Mode {theme === 'dark' ? 'clair' : 'sombre'}
    </button>
  )
}

// 4. Wrapper l'application
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <Footer />
    </ThemeProvider>
  )
}

Prêt à réussir tes entretiens React / Next.js ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Patterns avancés

11. Qu'est-ce qu'un Higher-Order Component (HOC) ?

Un HOC est une fonction qui prend un composant et retourne un nouveau composant enrichi. Pattern moins utilisé depuis les hooks mais toujours présent dans certaines bibliothèques.

jsx
// HOC qui ajoute des logs
function withLogging(WrappedComponent) {
  return function WithLogging(props) {
    useEffect(() => {
      console.log(`${WrappedComponent.name} monté avec props:`, props)

      return () => {
        console.log(`${WrappedComponent.name} démonté`)
      }
    }, [])

    return <WrappedComponent {...props} />
  }
}

// HOC qui gère l'authentification
function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const { user, isLoading } = useAuth()

    if (isLoading) return <LoadingSpinner />
    if (!user) return <Navigate to="/login" />

    return <WrappedComponent {...props} user={user} />
  }
}

// Utilisation
const ProtectedDashboard = withAuth(Dashboard)
const LoggedButton = withLogging(Button)

12. Expliquez le pattern Render Props

Le pattern Render Props permet de partager de la logique entre composants via une prop qui est une fonction.

jsx
// Composant avec render prop
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  useEffect(() => {
    const handleMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY })
    }

    window.addEventListener('mousemove', handleMove)
    return () => window.removeEventListener('mousemove', handleMove)
  }, [])

  // Appelle la fonction render avec les données
  return render(position)
}

// Utilisation
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <div>
          Position : {x}, {y}
        </div>
      )}
    />
  )
}

// Version moderne avec hook personnalisé (préférée)
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  useEffect(() => {
    const handleMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY })
    }

    window.addEventListener('mousemove', handleMove)
    return () => window.removeEventListener('mousemove', handleMove)
  }, [])

  return position
}

function App() {
  const { x, y } = useMousePosition()
  return <div>Position : {x}, {y}</div>
}

13. Comment créer un hook personnalisé ?

Les hooks personnalisés permettent d'extraire et réutiliser de la logique stateful entre composants.

useLocalStorage.jsjsx
function useLocalStorage(key, initialValue) {
  // Initialiser avec la valeur du localStorage ou la valeur par défaut
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(error)
      return initialValue
    }
  })

  // Wrapper du setter qui synchronise avec localStorage
  const setValue = useCallback((value) => {
    try {
      // Supporter les fonctions de mise à jour
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error(error)
    }
  }, [key, storedValue])

  return [storedValue, setValue]
}

// Utilisation
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16)

  return (
    <div>
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">Clair</option>
        <option value="dark">Sombre</option>
      </select>

      <input
        type="range"
        min="12"
        max="24"
        value={fontSize}
        onChange={e => setFontSize(Number(e.target.value))}
      />
    </div>
  )
}

14. Qu'est-ce que le Compound Components Pattern ?

Ce pattern permet de créer des composants qui fonctionnent ensemble de manière implicite, comme les balises <select> et <option>.

jsx
// Contexte partagé entre les composants
const TabsContext = createContext()

function Tabs({ children, defaultValue }) {
  const [activeTab, setActiveTab] = useState(defaultValue)

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  )
}

function TabList({ children }) {
  return <div className="tab-list" role="tablist">{children}</div>
}

function Tab({ value, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext)
  const isActive = activeTab === value

  return (
    <button
      role="tab"
      aria-selected={isActive}
      className={`tab ${isActive ? 'active' : ''}`}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  )
}

function TabPanel({ value, children }) {
  const { activeTab } = useContext(TabsContext)

  if (activeTab !== value) return null

  return (
    <div role="tabpanel" className="tab-panel">
      {children}
    </div>
  )
}

// Attacher les sous-composants
Tabs.List = TabList
Tabs.Tab = Tab
Tabs.Panel = TabPanel

// Utilisation intuitive
function App() {
  return (
    <Tabs defaultValue="profile">
      <Tabs.List>
        <Tabs.Tab value="profile">Profil</Tabs.Tab>
        <Tabs.Tab value="settings">Paramètres</Tabs.Tab>
        <Tabs.Tab value="billing">Facturation</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="profile">Contenu profil...</Tabs.Panel>
      <Tabs.Panel value="settings">Contenu paramètres...</Tabs.Panel>
      <Tabs.Panel value="billing">Contenu facturation...</Tabs.Panel>
    </Tabs>
  )
}

15. Comment implémenter le Controlled vs Uncontrolled pattern ?

Les composants contrôlés ont leur état géré par React, les non-contrôlés utilisent le DOM directement.

jsx
// Composant CONTRÔLÉ
// L'état est dans React, le composant reflète cet état
function ControlledInput() {
  const [value, setValue] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Valeur soumise:', value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <button type="submit">Envoyer</button>
    </form>
  )
}

// Composant NON CONTRÔLÉ
// L'état est dans le DOM, on le lit quand nécessaire
function UncontrolledInput() {
  const inputRef = useRef(null)

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Valeur soumise:', inputRef.current.value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={inputRef}
        defaultValue=""
      />
      <button type="submit">Envoyer</button>
    </form>
  )
}

// Composant qui supporte les DEUX modes
function FlexibleInput({ value, defaultValue, onChange }) {
  const isControlled = value !== undefined
  const [internalValue, setInternalValue] = useState(defaultValue || '')

  const currentValue = isControlled ? value : internalValue

  const handleChange = (e) => {
    if (!isControlled) {
      setInternalValue(e.target.value)
    }
    onChange?.(e.target.value)
  }

  return (
    <input
      type="text"
      value={currentValue}
      onChange={handleChange}
    />
  )
}

Performance et optimisation

16. Comment fonctionne React.memo ?

React.memo est un HOC qui mémorise un composant pour éviter les re-renders si les props n'ont pas changé.

jsx
// Composant mémoïsé
const ExpensiveList = React.memo(function ExpensiveList({ items, onItemClick }) {
  console.log('ExpensiveList rendered')

  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  )
})

// Parent qui utilise le composant mémoïsé
function Parent() {
  const [count, setCount] = useState(0)
  const [items] = useState([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ])

  // ❌ Cette fonction est recréée à chaque rendu
  // Donc ExpensiveList re-render malgré memo
  const handleClick = (id) => console.log(id)

  // ✅ Fonction stable avec useCallback
  const handleClickStable = useCallback((id) => {
    console.log(id)
  }, [])

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <ExpensiveList items={items} onItemClick={handleClickStable} />
    </div>
  )
}

// Comparaison personnalisée
const MemoWithCustomCompare = React.memo(
  function Component({ user, onClick }) {
    return <div onClick={onClick}>{user.name}</div>
  },
  (prevProps, nextProps) => {
    // Retourner true si les props sont égales (skip re-render)
    return prevProps.user.id === nextProps.user.id
  }
)

17. Qu'est-ce que le code splitting et React.lazy ?

Le code splitting permet de diviser le bundle en chunks chargés à la demande, réduisant le temps de chargement initial.

jsx
// Lazy loading de composants
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))
const Profile = lazy(() => import('./Profile'))

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Suspense>
  )
}

// Lazy loading conditionnel
function FeaturePanel({ showAdvanced }) {
  // Le composant n'est chargé que si nécessaire
  const AdvancedOptions = lazy(() => import('./AdvancedOptions'))

  return (
    <div>
      <BasicOptions />

      {showAdvanced && (
        <Suspense fallback={<Skeleton />}>
          <AdvancedOptions />
        </Suspense>
      )}
    </div>
  )
}

// Named exports avec lazy
const { Chart } = lazy(() =>
  import('./Charts').then(module => ({ default: module.Chart }))
)

18. Comment optimiser les listes longues ?

Les listes longues peuvent causer des problèmes de performance. La virtualisation ne rend que les éléments visibles.

jsx
// Avec react-window (bibliothèque de virtualisation)
import { FixedSizeList } from 'react-window'

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      {items[index].name}
    </div>
  )

  return (
    <FixedSizeList
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  )
}

// Optimisation sans bibliothèque : pagination
function PaginatedList({ items, pageSize = 20 }) {
  const [page, setPage] = useState(0)

  const paginatedItems = useMemo(() => {
    const start = page * pageSize
    return items.slice(start, start + pageSize)
  }, [items, page, pageSize])

  const totalPages = Math.ceil(items.length / pageSize)

  return (
    <div>
      <ul>
        {paginatedItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>

      <div className="pagination">
        <button
          onClick={() => setPage(p => p - 1)}
          disabled={page === 0}
        >
          Précédent
        </button>
        <span>{page + 1} / {totalPages}</span>
        <button
          onClick={() => setPage(p => p + 1)}
          disabled={page >= totalPages - 1}
        >
          Suivant
        </button>
      </div>
    </div>
  )
}
Seuil de virtualisation

La virtualisation devient pertinente au-delà de quelques centaines d'éléments. Pour des listes plus petites, la pagination ou le chargement progressif sont souvent suffisants.

19. Comment éviter les re-renders inutiles ?

Identifier et éliminer les re-renders inutiles est crucial pour les performances. Plusieurs techniques permettent d'optimiser le rendu.

jsx
// Technique 1 : Séparer les composants
// ❌ Tout re-render quand count change
function BadExample() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <ExpensiveComponent /> {/* Re-render inutile */}
    </div>
  )
}

// ✅ ExpensiveComponent ne re-render plus
function GoodExample() {
  return (
    <div>
      <Counter />
      <ExpensiveComponent />
    </div>
  )
}

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

// Technique 2 : Passer children au lieu de rendre directement
// ✅ children est stable, pas de re-render des enfants
function ContextProvider({ children }) {
  const [value, setValue] = useState(0)

  return (
    <Context.Provider value={value}>
      {children} {/* children ne re-render pas quand value change */}
    </Context.Provider>
  )
}

// Technique 3 : Utiliser les DevTools pour identifier les re-renders
// React DevTools > Profiler > "Highlight updates when components render"

20. Qu'est-ce que le batching automatique dans React 18+ ?

React 18 regroupe automatiquement les mises à jour d'état pour réduire le nombre de re-renders.

jsx
// Avant React 18 : deux re-renders
function handleClick() {
  setCount(c => c + 1) // Re-render
  setFlag(f => !f)      // Re-render
}

// React 18+ : un seul re-render (batching automatique)
function handleClick() {
  setCount(c => c + 1) // Batché
  setFlag(f => !f)      // Batché
  // Un seul re-render à la fin
}

// Même dans les callbacks async (nouveauté React 18)
async function handleSubmit() {
  const response = await fetch('/api/submit')

  // Ces deux mises à jour sont batchées
  setData(response.data)
  setLoading(false)
  // Un seul re-render
}

// Pour forcer un re-render immédiat (rare)
import { flushSync } from 'react-dom'

function handleClick() {
  flushSync(() => {
    setCount(c => c + 1)
  })
  // DOM mis à jour ici

  flushSync(() => {
    setFlag(f => !f)
  })
  // DOM mis à jour ici
}

Prêt à réussir tes entretiens React / Next.js ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

React moderne (18+)

21. Expliquez useTransition et useDeferredValue

Ces hooks permettent de marquer des mises à jour comme non-urgentes pour maintenir la réactivité de l'interface.

jsx
// useTransition : marquer une mise à jour comme non-urgente
function SearchResults() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  const handleChange = (e) => {
    // Mise à jour urgente : l'input reste réactif
    setQuery(e.target.value)

    // Mise à jour non-urgente : peut être interrompue
    startTransition(() => {
      const filtered = filterLargeDataset(e.target.value)
      setResults(filtered)
    })
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />

      {isPending && <span>Recherche...</span>}

      <ul>
        {results.map(r => <li key={r.id}>{r.name}</li>)}
      </ul>
    </div>
  )
}

// useDeferredValue : différer une valeur
function DeferredSearch() {
  const [query, setQuery] = useState('')
  // deferredQuery est en "retard" pendant les mises à jour rapides
  const deferredQuery = useDeferredValue(query)

  // Montrer un indicateur pendant le "retard"
  const isStale = query !== deferredQuery

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />

      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        {/* Utilise la valeur différée, moins de calculs */}
        <ExpensiveList filter={deferredQuery} />
      </div>
    </div>
  )
}

22. Comment fonctionne Suspense pour le data fetching ?

Suspense permet de gérer les états de chargement de manière déclarative. Avec React 18+, il s'étend au data fetching.

Avec une bibliothèque compatible (React Query, Relay, etc.)jsx
function UserProfile({ userId }) {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <ProfileContent userId={userId} />
    </Suspense>
  )
}

// Le composant "suspend" pendant le chargement
function ProfileContent({ userId }) {
  // Cette fonction suspend si les données ne sont pas prêtes
  const user = useUserData(userId)

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  )
}

// Suspense boundaries imbriquées
function Dashboard() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <Header />

      <div className="grid">
        <Suspense fallback={<StatsSkeleton />}>
          <Stats />
        </Suspense>

        <Suspense fallback={<ChartSkeleton />}>
          <Chart />
        </Suspense>

        <Suspense fallback={<TableSkeleton />}>
          <RecentActivity />
        </Suspense>
      </div>
    </Suspense>
  )
}

23. Qu'est-ce que useActionState (React 19) ?

useActionState (anciennement useFormState) simplifie la gestion des formulaires avec Server Actions.

actions.jsjsx
'use server'

async function submitForm(prevState, formData) {
  const email = formData.get('email')
  const password = formData.get('password')

  // Validation
  if (!email || !password) {
    return { error: 'Tous les champs sont requis' }
  }

  try {
    await createUser({ email, password })
    return { success: true, message: 'Compte créé !' }
  } catch (e) {
    return { error: e.message }
  }
}

// Component
function SignupForm() {
  const [state, formAction, isPending] = useActionState(submitForm, {})

  return (
    <form action={formAction}>
      {state.error && (
        <div className="error">{state.error}</div>
      )}

      {state.success && (
        <div className="success">{state.message}</div>
      )}

      <input name="email" type="email" placeholder="Email" />
      <input name="password" type="password" placeholder="Mot de passe" />

      <button type="submit" disabled={isPending}>
        {isPending ? 'Création...' : 'Créer un compte'}
      </button>
    </form>
  )
}

24. Comment fonctionnent les Server Components ?

Les Server Components s'exécutent uniquement sur le serveur, réduisant le JavaScript envoyé au client.

ServerComponent.jsxjsx
// Pas de "use client" = Server Component par défaut
import { prisma } from '@/lib/prisma'

// async/await directement dans le composant
export default async function ProductList() {
  // Appel base de données direct (pas d'API)
  const products = await prisma.product.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10
  })

  return (
    <div>
      <h2>Produits récents</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name} - {product.price}            {/* On peut inclure des Client Components */}
            <AddToCartButton productId={product.id} />
          </li>
        ))}
      </ul>
    </div>
  )
}

// AddToCartButton.jsx
'use client'

// Ce composant a besoin d'interactivité
export function AddToCartButton({ productId }) {
  const [isPending, startTransition] = useTransition()

  const handleClick = () => {
    startTransition(async () => {
      await addToCart(productId)
    })
  }

  return (
    <button onClick={handleClick} disabled={isPending}>
      {isPending ? '...' : 'Ajouter'}
    </button>
  )
}

25. Expliquez le streaming SSR avec React 18

Le streaming SSR envoie le HTML progressivement au navigateur au lieu d'attendre le rendu complet.

app/page.jsx (Next.js App Router)jsx
import { Suspense } from 'react'

export default function Page() {
  return (
    <div>
      {/* Rendu immédiat */}
      <Header />

      {/* Streamé quand prêt */}
      <Suspense fallback={<MainContentSkeleton />}>
        <MainContent />
      </Suspense>

      {/* Streamé indépendamment */}
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>

      {/* Rendu immédiat */}
      <Footer />
    </div>
  )
}

// Le navigateur reçoit :
// 1. Shell HTML avec Header, skeletons, Footer
// 2. MainContent quand sa requête est terminée
// 3. Sidebar quand sa requête est terminée

// loading.jsx pour le streaming au niveau route
export default function Loading() {
  return <PageSkeleton />
}

State management

26. Quand utiliser Redux vs Context API vs autres solutions ?

Le choix dépend de la complexité de l'état et des besoins du projet.

jsx
// Context API : état simple, peu de mises à jour
// ✅ Bon pour : thème, utilisateur, préférences
const ThemeContext = createContext()

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// Redux / Zustand : état complexe, mises à jour fréquentes
// ✅ Bon pour : panier, filtres, données métier complexes
import { create } from 'zustand'

const useCartStore = create((set, get) => ({
  items: [],
  total: 0,

  addItem: (product) => set((state) => {
    const existing = state.items.find(i => i.id === product.id)
    if (existing) {
      return {
        items: state.items.map(i =>
          i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
        )
      }
    }
    return { items: [...state.items, { ...product, quantity: 1 }] }
  }),

  removeItem: (id) => set((state) => ({
    items: state.items.filter(i => i.id !== id)
  })),

  getTotal: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0)
}))

// React Query / SWR : état serveur (cache, refetch)
// ✅ Bon pour : données API, synchronisation serveur
import { useQuery, useMutation } from '@tanstack/react-query'

function Products() {
  const { data, isLoading } = useQuery({
    queryKey: ['products'],
    queryFn: () => fetch('/api/products').then(r => r.json())
  })

  const mutation = useMutation({
    mutationFn: (newProduct) => fetch('/api/products', {
      method: 'POST',
      body: JSON.stringify(newProduct)
    })
  })
}

27. Comment implémenter le pattern Reducer ?

Le pattern Reducer centralise la logique de mise à jour d'état complexe.

jsx
// Définir les actions
const ACTIONS = {
  ADD_TODO: 'ADD_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO',
  DELETE_TODO: 'DELETE_TODO',
  SET_FILTER: 'SET_FILTER'
}

// Reducer pur (sans effets de bord)
function todoReducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, completed: false }
        ]
      }

    case ACTIONS.TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      }

    case ACTIONS.DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      }

    case ACTIONS.SET_FILTER:
      return { ...state, filter: action.payload }

    default:
      return state
  }
}

// Utilisation avec useReducer
function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: 'all'
  })

  const addTodo = (text) => {
    dispatch({ type: ACTIONS.ADD_TODO, payload: text })
  }

  const toggleTodo = (id) => {
    dispatch({ type: ACTIONS.TOGGLE_TODO, payload: id })
  }

  const filteredTodos = useMemo(() => {
    switch (state.filter) {
      case 'completed':
        return state.todos.filter(t => t.completed)
      case 'active':
        return state.todos.filter(t => !t.completed)
      default:
        return state.todos
    }
  }, [state.todos, state.filter])

  return (
    <div>
      <TodoForm onAdd={addTodo} />
      <TodoList todos={filteredTodos} onToggle={toggleTodo} />
      <FilterButtons
        current={state.filter}
        onChange={(f) => dispatch({ type: ACTIONS.SET_FILTER, payload: f })}
      />
    </div>
  )
}

Testing

28. Comment tester un composant React ?

Les tests de composants vérifient le rendu et le comportement de l'interface.

Button.test.jsxjsx
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Button from './Button'

describe('Button', () => {
  // Test de rendu basique
  it('renders with correct text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button')).toHaveTextContent('Click me')
  })

  // Test d'interaction
  it('calls onClick when clicked', async () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click</Button>)

    await userEvent.click(screen.getByRole('button'))

    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  // Test d'état disabled
  it('does not call onClick when disabled', async () => {
    const handleClick = jest.fn()
    render(<Button disabled onClick={handleClick}>Click</Button>)

    await userEvent.click(screen.getByRole('button'))

    expect(handleClick).not.toHaveBeenCalled()
  })

  // Test avec état asynchrone
  it('shows loading state', async () => {
    render(<Button isLoading>Submit</Button>)

    expect(screen.getByRole('button')).toBeDisabled()
    expect(screen.getByTestId('spinner')).toBeInTheDocument()
  })
})

// Test d'un hook personnalisé
import { renderHook, act } from '@testing-library/react'
import useCounter from './useCounter'

describe('useCounter', () => {
  it('increments counter', () => {
    const { result } = renderHook(() => useCounter(0))

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })
})

29. Comment mocker les appels API dans les tests ?

Les mocks isolent les tests des dépendances externes.

jsx
// Avec MSW (Mock Service Worker) - recommandé
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'

const server = setupServer(
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ])
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json()
    return HttpResponse.json({ id: 3, ...body }, { status: 201 })
  })
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

test('loads and displays users', async () => {
  render(<UserList />)

  expect(screen.getByText('Loading...')).toBeInTheDocument()

  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument()
    expect(screen.getByText('Bob')).toBeInTheDocument()
  })
})

// Override pour un test spécifique
test('handles error state', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ error: 'Server error' }, { status: 500 })
    })
  )

  render(<UserList />)

  await waitFor(() => {
    expect(screen.getByText('Erreur de chargement')).toBeInTheDocument()
  })
})

30. Comment structurer les tests pour une couverture optimale ?

Une stratégie de test équilibrée combine différents niveaux de tests.

jsx
// Pyramide de tests recommandée :
// - Beaucoup de tests unitaires (rapides, isolés)
// - Quelques tests d'intégration (composants + hooks)
// - Peu de tests E2E (scénarios critiques)

// Tests unitaires : logique pure
// utils/formatPrice.test.js
describe('formatPrice', () => {
  it('formats with 2 decimals', () => {
    expect(formatPrice(10)).toBe('10.00 €')
  })

  it('handles zero', () => {
    expect(formatPrice(0)).toBe('0.00 €')
  })
})

// Tests d'intégration : composant complet
// features/Checkout/Checkout.test.jsx
describe('Checkout', () => {
  it('completes purchase flow', async () => {
    render(
      <CartProvider>
        <Checkout />
      </CartProvider>
    )

    // Remplir le formulaire
    await userEvent.type(screen.getByLabelText('Email'), 'test@example.com')
    await userEvent.type(screen.getByLabelText('Carte'), '4242424242424242')

    // Soumettre
    await userEvent.click(screen.getByRole('button', { name: 'Payer' }))

    // Vérifier le résultat
    await waitFor(() => {
      expect(screen.getByText('Commande confirmée')).toBeInTheDocument()
    })
  })
})

// Tests E2E avec Playwright
// e2e/checkout.spec.ts
test('user can complete checkout', async ({ page }) => {
  await page.goto('/products')
  await page.click('[data-testid="add-to-cart-1"]')
  await page.click('[data-testid="checkout-button"]')

  await page.fill('[name="email"]', 'test@example.com')
  await page.fill('[name="card"]', '4242424242424242')
  await page.click('button[type="submit"]')

  await expect(page.locator('text=Commande confirmée')).toBeVisible()
})

Conclusion

Ces 30 questions couvrent l'essentiel des connaissances React attendues en entretien. Les points clés à maîtriser :

  • Fondamentaux : Virtual DOM, JSX, props vs state, composants
  • Hooks : useState, useEffect, useMemo, useCallback, useRef, useContext
  • Patterns : HOC, Render Props, Compound Components, hooks personnalisés
  • Performance : React.memo, lazy loading, virtualisation
  • React moderne : Suspense, Transitions, Server Components
  • State management : Context, Redux/Zustand, React Query
  • Testing : React Testing Library, mocks, stratégie de test

La préparation aux entretiens React ne se limite pas à la mémorisation. Pratiquer avec des projets concrets permet de consolider ces concepts et de les expliquer naturellement lors de l'entretien.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#react interview
#frontend interview
#react questions
#javascript
#entretien technique

Partager

Articles similaires