Accueil / Articles / Comment React-Redux décide de rérender : les sélecteurs, l’égalité et les hooks typés.

Comment React-Redux décide de rérender : les sélecteurs, l’égalité et les hooks typés.

Un guide d’étude sur les mécanismes internes de React-Redux : Provider, l’égalité avec useSelector, les sélecteurs mémorisés, connect(), les hooks typés avec withTypes(), ainsi que les cas limites rares.

7738 mots

La surface d’interaction de React-Redux semble minime : un composant provider et quelques hooks. Pourtant, dans une grande base de code, les mêmes questions reviennent sans cesse. Pourquoi ce composant est-il rendu à chaque dispatch ? Pourquoi un sélecteur qui renvoie un objet se comporte-t-il différemment de mapStateToProps ? Quand shallowEqual est-il l’outil approprié, que décrivent réellement RootState et AppDispatch, et le batch() est-il encore nécessaire en React 18 ?

Ce guide répond à ces questions en suivant une logique cohérente : ce qui se passe entre une action envoyée et un nouveau rendu. Une fois cette démarche claire, les différentes API cessent d’être une liste à mémoriser pour devenir des éléments d’un système que l’on peut comprendre, déboguer et expliquer lors d’une revue de code ou d’un entretien.

useSelector()
useDispatch()
<Provider />

Conseils actuels : les mainteneurs de React-Redux recommandent l’API des Hooks comme solution par défaut pour les composants. connect() reste pris en charge et mérite d’être connu, car de nombreux codes existants s’y appuient encore.

Qu’est-ce que React-Redux et quelle est sa place

Redux gère l’état, exécute les reducers et informe les écouteurs ; React rend l’interface utilisateur. React-Redux est le lien officiel qui permet aux composants de lire les données du store et de déclencher des mises à jour, tandis que React reste chargé du rendu.

Redux
  │
  │ Application State
  ▼
React-Redux
  │
  │ Integration
  ▼
React
  │
  │ UI
  ▼
User

Concrètement, ce lien confère aux composants quatre capacités :

  • lire des valeurs depuis l’état Redux
  • s’abonner aux modifications de ces valeurs
  • envoyer des actions vers le store
  • intégrer les mises à jour du store dans le cycle de rendu de React

Les points d’entrée modernes pour ces opérations sont trois hooks :

useSelector()
useDispatch()
useStore()

de plus, le composant qui rend le store accessible en premier lieu :

<Provider />

Pour du code plus ancien, il existe également l’API de composant d’ordre supérieur, qui reste pleinement prise en charge :

connect()

Redux et React-Redux sont des packages différents

Redux lui-même est le conteneur d’état et possède ces concepts :

Store
State
Actions
Reducers
Dispatch
Middleware
Selectors

React-Redux n’est qu’un pont, et tout ce qu’il exporte concerne la communication avec React :

Provider
useSelector
useDispatch
useStore
connect

Assemblés ensemble, les couches ressemblent à ceci :

              Redux
        ┌───────────────┐
        │ Store         │
        │ State         │
        │ Reducers      │
        │ Actions       │
        │ Middleware    │
        └───────┬───────┘
                │
                ▼
          React-Redux
        ┌───────────────┐
        │ Provider      │
        │ useSelector   │
        │ useDispatch   │
        │ useStore      │
        │ connect       │
        └───────┬───────┘
                │
                ▼
              React

Si une question concerne la manière dont l’état change (réducteurs, middleware, formats d’actions), elle relève de Redux. Si elle concerne le moment où un composant détecte un changement ou se rend, elle relève de React-Redux.

Le cycle fondamental à retenir

Avant même d’utiliser une API, internalisez le cycle suivant : parcourir un sélecteur, traiter l’interaction, réduire l’état en un nouveau état, relancer le sélecteur, et ne pas afficher de contenu que si la valeur sélectionnée a changé.

                 React Component
                       │
                       │ useSelector()
                       ▼
                 Redux Store
                       │
                       │ current state
                       ▼
                    Component
                       │
                       │ user interaction
                       ▼
                 useDispatch()
                       │
                       │ dispatch(action)
                       ▼
                    Reducer
                       │
                       ▼
                  New Redux State
                       │
                       ▼
                 useSelector()
                       │
                       ▼
                Re-render if
                 selected value
                   changed

Presque toutes les questions de performance liées à React-Redux concernent cette dernière étape du diagramme.

Provider : rendre le store accessible

Les hooks ne peuvent communiquer qu’avec un store qui se trouve quelque part en amont dans l’arborescence. <Provider> est ce qui le place là. Généralement, on enroule une seule fois la racine de l’application :

import { Provider } from 'react-redux'
import { store } from './store'
function AppRoot() {
  return (
    <Provider store={store}>
      <App />
    </Provider>
  )
}

En réalité, le Provider place le store dans un contexte React. Tout descendant, quel que soit son niveau d’ancrage, peut alors y accéder sans avoir besoin de parcourir les props en profondeur :

<Provider store={store}>
       │
       ├── App
       │    ├── Header
       │    ├── Dashboard
       │    ├── UserProfile
       │    └── Settings
       │
       └── Every descendant
            can use Redux

Si un composant appelle l’un des hooks en dehors d’un Provider, il n’y a pas de store à lire et le hook échoue (en pratique, une erreur est générée indiquant que la valeur du contexte manque) :

useSelector(...)
useDispatch(...)

C’est souvent un problème dans les tests unitaires qui affichent un composant sans l’encadrer dans un Provider de test.

useSelector : comment les composants lisent l’état

useSelector() est le hook que vous utiliserez le plus souvent. Vous lui fournissez une fonction qui reçoit l’ensemble de l’état Redux et retourne la partie nécessaire au composant :

const count = useSelector(
  state => state.counter.value
)

La structure du flux de données est simple :

Redux State
     │
     ▼
useSelector()
     │
     ▼
Selected Value
     │
     ▼
React Component

Puisque React-Redux peut appeler votre sélecteur plus souvent que prévu (lors de l’affichage, après des dispatchs, pendant les vérifications en développement), le sélecteur doit être pur : même entrée, même sortie, sans effets secondaires.

Ce que fait le hook à chaque affichage et chaque dispatch

Prenez un sélecteur qui choisit l’utilisateur actuel :

const user = useSelector(
  state => state.auth.user
)

Derrière cette seule ligne, React-Redux exécute une petite routine :

  1. lit l’état actuel du store
  2. appelle votre sélecteur avec cet état
  3. conserve la valeur retournée comme le résultat « dernièrement sélectionné »
  4. enregistre une abonnement au store pour ce composant
  5. réexécute le sélecteur après chaque action envoyée
  6. compare le résultat précédent avec le nouveau
  7. planifie un re-render uniquement lorsque la comparaison indique une différence

C’est à l’étape 6 que proviennent presque tous les comportements surprenants.

La comparaison par défaut est l’égalité stricte de référence

Déjà prêt à l’emploi, le hook

useSelector()

compare les résultats avec ===:

previousResult === newResult

Lorsque la comparaison donne

true

le sélecteur ne donne pas à React-Redux de raison de rérender la composante. Lorsqu’il produit un résultat

false

la composante est programmée pour être rérender.

C’est une différence délibérée par rapport à connect(), qui effectue une comparaison superficielle de l’objet retourné par mapStateToProps. Du code qui fonctionnait bien avec connect() peut commencer à se rérender à chaque action une fois qu’il est transféré vers les hooks sans modification du sélecteur.

Pourquoi le retour d’un objet nouveau provoque une rérender à chaque fois

Une première tentative naturelle pour lire deux valeurs ressemble à ceci :

const data = useSelector(state => ({
  user: state.user,
  count: state.count
}))

Il n’y a rien de mal avec les valeurs, mais la fonction flèche crée un nouvel objet littéral à chaque exécution :

{
  user: ...,
  count: ...
}

Deux objets littéraux ayant des contenus identiques restent deux objets différents, donc la vérification

oldObject === newObject

donne toujours pour résultat

false

La conséquence est une chaîne d’appels qui s’exécute à chaque action dans l’application, y compris celles qui n’ont rien à voir avec les utilisateurs ou les comptages :

Action dispatched
      ↓
Selector executes
      ↓
New object created
      ↓
Different reference
      ↓
Component re-renders

Ce problème fait partie des erreurs de performance les plus fréquentes avec React-Redux. La documentation propose trois solutions : des sélecteurs distincts, une fonction d’égalité personnalisée telle que shallowEqual, ou un sélecteur mémorisé.

Résolution 1 : appeler useSelector une fois par valeur

Au lieu de regrouper les valeurs dans un objet,

const data = useSelector(state => ({
  user: state.user,
  count: state.count
}))

divisez la lecture en hooks indépendants :

const user = useSelector(
  state => state.user
)
const count = useSelector(
  state => state.count
)

Chaque sélecteur renvoie désormais une valeur déjà présente dans le store, et non un nouvel objet qui l’entoure. Lorsque l’état qui les contient n’a pas changé,

user unchanged
count unchanged

les références sont identiques et la vérification === réussit.

Il n’y a pas de pénalité pour appeler le hook plusieurs fois au sein d’un même composant. Si une seule invocation modifie plus d’une des valeurs sélectionnées, React-Redux regroupe les mises à jour résultantes, de sorte que le composant s’affiche une seule fois pour cette invocation plutôt qu’une fois par hook.

Résolution deux : shallowEqual pour les objets intentionnels

Parfois, un objet est vraiment la réponse la plus claire à retourner, par exemple lorsque un composant utilise un petit ensemble de champs liés. Dans ce cas, passez shallowEqual comme opérateur de comparaison :

import {
  useSelector,
  shallowEqual
} from 'react-redux';

const data = useSelector(
  state => ({
    user: state.user,
    count: state.count
  }),
  shallowEqual
)

Avec cet opérateur, React-Redux compare chaque champ de niveau supérieur des objets ancien et nouveau à l’aide de === au lieu de comparer les références des deux objets. Un nouvel objet enveloppant ayant les mêmes valeurs de champs est considéré comme inchangé.

Les versions récentes acceptent également la comparaison via un objet d’options :

const data = useSelector(
  selector,
  {
    equalityFn: shallowEqual
  }
)

Quand shallowEqual est utile

Préférez shallowEqual lorsque le regroupement des valeurs rend la composante plus lisible. Ne le considérez pas comme une option par défaut à appliquer systématiquement à chaque appel :

useSelector(selector, shallowEqual)

La première question pertinente est de savoir si la composante ne peut pas simplement sélectionner chaque valeur individuellement. La plupart du temps, c’est possible, et le résultat est plus facile à parcourir :

const user = useSelector(
  state => state.auth.user
)

const permissions = useSelector(
  state => state.auth.permissions
)

N’oubliez pas que shallowEqual ne vérifie que le premier niveau de profondeur. Si un champ à l’intérieur de l’objet retourné est lui-même un tableau ou un objet nouvellement créé, la comparaison échoue néanmoins.

Sélecteurs qui dérivent des données

Les sélecteurs servent à bien plus que de simplement extraire des champs. C’est aussi là que l’on calcule des données dérivées, comme une liste filtrée :

const selectCompletedTodos = state =>
  state.todos.filter(
    todo => todo.completed
  )

Le problème, c’est que

filter()

alloue toujours un nouveau tableau. Même lorsque la liste des tâches n’a pas changé de manière significative,

oldArray !== newArray

Il conserve les données, et le composant se rend après chaque action. La mémorisation résout ce problème en stockant en cache la sortie en fonction des entrées. Lorsque les entrées sont les mêmes objets que la dernière fois, on obtient à nouveau le résultat en cache :

Same inputs
    ↓
Return cached result

Lorsqu’une entrée a changé, le calcul est exécuté à nouveau :

Changed inputs
    ↓
Recalculate

createSelector de Reselect

Outil standard pour cela est createSelector, provenant de Reselect (Redux Toolkit le réexporte). On y liste des sélecteurs d’entrée, puis une fonction de résultat qui ne s’exécute que lorsque ces entrées changent :

import { createSelector } from 'reselect';

const selectCompletedTodos = createSelector(
  state => state.todos,
  todos =>
    todos.filter(todo => todo.completed)
)

Le composant l’utilise comme n’importe quel autre sélecteur :

const todos = useSelector(
  selectCompletedTodos
)

Puisque state.todos est la même référence d’array, le sélecteur renvoie son array filtré précédent, ce qui fait que === est vrai. Attention : un sélecteur mémorisé conserve une cache, donc l’endroit où son instance est créée est important.

Créez les sélecteurs mémorisés basés uniquement sur l’état une seule fois, au niveau du module

Lorsqu’un sélecteur mémorisé ne dépend que de l’état Redux,

const selectCompletedTodos = createSelector(
  state => state.todos,
  todos => ...
)

declarez-le en dehors de tout composant :

const selectCompletedTodos = createSelector(...)

et faites référence à lui depuis le composant :

function TodoList() {
  const todos = useSelector(
    selectCompletedTodos
  )
}

Si createSelector est exécuté à l’intérieur du corps du composant, chaque rendu créera un nouveau sélecteur avec un cache vide, et la mémorisation ne servira à rien. Une instance au niveau du module persiste entre les rendus.

Sélecteurs mémorisés qui nécessitent également des props

Les sélecteurs simples, non mémorisés, qui lisent des props ne posent aucun problème. Celui-ci ne conserve pas de cache, donc il n’y a rien qui puisse mal se passer :

function TodoListItem({ id }) {
  const todo = useSelector(
    state => state.todos[id]
  )
return <div>{todo.text}</div>
}

Les choses deviennent plus subtiles lorsque un sélecteur mémorisé dépend à la fois de l’état et des props.

Redux State + Component Props

Un tel sélecteur met en cache le résultat pour ses derniers arguments. Si de nombreux éléments d’une liste partagent une même instance, chacun ayant un id différent, ils invalident continuellement la mémoire cache de l’autre. Pour une seule composante, il suffit généralement de créer le sélecteur à l’aide de useMemo ; pour plusieurs, il est nécessaire de connaître la stratégie de mémorisation de votre bibliothèque (taille de la cache ou une instance par composante). Ce sujet apparaît lors des entretiens pour des postes avancés ainsi que dans le cas de listes longues.

Les sélecteurs doivent rester purs

Un sélecteur doit être une simple fonction de l’état :

State
  ↓
Selector
  ↓
Value

et jamais un endroit où des opérations s’échappent vers l’extérieur :

State
  ↓
Selector
  ↓
API call
  ↓
Mutation
  ↓
Side effect

Ce type de sélecteur doit être évité ; l’enregistrement de logs, les appels réseau et les mutations ne doivent pas y figurer :

const selectUser = state => {
  console.log('side effect')
  // API call ❌
  // mutation ❌
  return state.user
}

Utilisation de props à l’intérieur d’un sélecteur

Un sélecteur défini en ligne dans une composante peut simplement faire référence aux props de cette composante :

function TodoItem({ id }) {
  const todo = useSelector(
    state => state.todos[id]
  )
return <div>{todo.text}</div>
}

La valeur passe de la propriété au sélecteur via une fermeture :

id
 ↓
closure
 ↓
selector
 ↓
state.todos[id]

C’est là une différence réelle par rapport à mapStateToProps, qui reçoit ownProps en tant que deuxième argument. useSelector() ne transmet aucune propriété, il faut donc compter sur des fermetures ou des fonctions de sélecteur qui prennent des arguments supplémentaires.

useDispatch : envoi d’actions

Si useSelector() représente la partie de lecture,

useSelector
     ↓
    READ

alors useDispatch() représente la partie d’écriture :

useDispatch
     ↓
  DISPATCH

Il retourne la fonction dispatch du store, que l’on appelle depuis les gestionnaires d’événements :

const dispatch = useDispatch()

function handleClick() {
  dispatch(increment())
}

ou directement dans le JSX :

<button
  onClick={() => dispatch(increment())}
>
  Increment
</button>

Ce que dispatch met en mouvement

Suivre un clic de bouton du début à la fin renforce le cycle principal évoqué précédemment :

User clicks button
       ↓
dispatch(action)
       ↓
Redux
       ↓
Reducer
       ↓
New state
       ↓
useSelector()
       ↓
Component updates

Une invocation concrète avec un créateur d’action et une charge utile se présente comme ceci :

dispatch(
  addTodo({
    id: 1,
    text: 'Learn Redux'
  })
)

Appels de retour stables pour les enfants mémorisés

La plupart des appels de retour lors du dispatch n’ont pas besoin de useCallback. L’exception concerne les gestionnaires tels que

const increment = () =>
  dispatch(incrementAction())

qui sont transmis à un enfant enveloppé dans React.memo :

<MyButton
  onIncrement={increment}
/>

Puisque la fonction flèche est recréée à chaque rendu du parent, l’enfant mémorisé reçoit une nouvelle propriété à chaque fois et se rend quand même. Envelopper le gestionnaire résout ce problème :

const increment = useCallback(
  () => dispatch(incrementAction()),
  [dispatch]
)

en même temps qu’un enfant mémorisé :

const MyButton = React.memo(...)

Indiquer dispatch comme dépendance est sûr : son identité reste la même tant que le Provider reçoit la même instance de stockage.

useStore : accès direct, rarement nécessaire

Le troisième hook vous fournit l’objet de stockage lui-même :

const store = useStore()

Avec cela, vous pouvez appeler les méthodes brutes du store :

store.getState()
store.dispatch(...)
store.subscribe(...)

Cet accès n’est presque jamais ce qu’un composant devrait utiliser pour afficher des données. La lecture s’effectue via

useSelector()

et l’écriture s’effectue via

useDispatch()

La documentation considère useStore() comme une solution de secours pour des cas rares, tels que l’injection d’un réducteur, et non pour les lectures quotidiennes.

Pourquoi store.getState() dans render devient obsolète

Considérez un composant qui lit directement dans le store :

function Component() {
  const store = useStore()
  const user = store.getState().user
  return <div>{user.name}</div>
}

Celui-ci s’affiche correctement une fois, puis reste dépassé. getState() est une lecture ponctuelle sans abonnement, de sorte qu’un changement ultérieur ne pousse jamais React à réafficher ce composant. La version avec abonnement reste synchronisée :

const user = useSelector(
  state => state.user
)

Un aperçu des trois hooks

┌────────────────────────────┐
│ React-Redux Hooks          │
├────────────────────────────┤
│ useSelector()              │ → READ
│ useDispatch()              │ → DISPATCH
│ useStore()                 │ → STORE ACCESS
└────────────────────────────┘

Dans le code quotidien des composants, les deux premiers effectuent presque tout le travail :

90%+
useSelector()
useDispatch()

useStore() n’apparaît que de temps en temps.

connect() : l’API du composant d’ordre supérieur

Les hooks sont la méthode recommandée, mais connect() n’a pas disparu et de nombreux projets anciens reposent encore sur lui. On peut rencontrer du code comme celui-ci :

connect(
  mapStateToProps,
  mapDispatchToProps
)(Component)

Comment connect transforme le store en props

Le modèle mental est celui d’un enveloppeur qui convertit les données du store et les fonctions dispatch en props ordinaires :

Redux Store
     │
     ▼
connect()
     │
     ├── mapStateToProps
     │
     └── mapDispatchToProps
     │
     ▼
Component Props

mapStateToProps reçoit l’état et renvoie un objet de props :

const mapStateToProps = state => ({
  user: state.auth.user,
  count: state.counter.value
})

En réalité, il effectue cette transformation :

Redux State
     ↓
Component Props

Le composant enveloppé reste une simple fonction de ses props et ne sait pas que Redux existe :

function User({ user, count }) {
  return (
    <div>
      {user.name}
      {count}
    </div>
  )
}

mapDispatchToProps fournit les fonctions de rappel. Dans sa forme fonctionnelle, vous recevez dispatch et vous créez vous-même les gestionnaires d’événements :

const mapDispatchToProps =
  dispatch => ({
    increment: () =>
      dispatch(increment())
  })

La composante les appelle ensuite en tant que propriétés :

props.increment()

La forme abrégée de mapDispatchToProps

La forme plus concise passe un objet contenant des créateurs d’actions :

const mapDispatchToProps = {
  increment,
  decrement
}

React-Redux lie chaque créateur d’actions de sorte que l’appel à la propriété le déclenche. C’est généralement l’option la plus propre.

Les quatre formes courantes de connect

Vous rencontrerez quatre variantes. Sans arguments, la composante ne reçoit que dispatch en tant que propriété :

connect()(Component)

Avec uniquement un mappage d’état, la composante lit les données mais ne reçoit pas de créateurs d’actions liés :

connect(
  mapStateToProps
)(Component)

Avec null comme premier argument, le composant n’écoute jamais le store et ne reçoit que les props de dispatch :

connect(
  null,
  mapDispatchToProps
)(Component)

Et avec les deux, il lit et envoie des actions :

connect(
  mapStateToProps,
  mapDispatchToProps
)(Component)

Savoir que le formulaire null évite l’abonnement est utile : c’est un moyen simple de donner à un composant un accès au dispatch sans qu’il se rérendera en cas de mise à jour du store.

connect retourne un nouveau composant

L’appel

connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent)

n’altère pas MyComponent. Il génère un composant d’enveloppe distinct qui affiche le vôtre à l’intérieur de lui :

MyComponent
     │
     ▼
connect()
     │
     ▼
ConnectedComponent

C’est pourquoi les modules connectés exportent généralement la version enveloppée par défaut et exportent parfois le composant simple séparément pour les tests.

Sélection entre hooks et connect

Pour une nouvelle application, la réponse est simple :

New React application
        ↓
Hooks

Pour un cas existant, la solution est pragmatique :

Existing connect()
        ↓
Understand and maintain it

Les hooks réduisent le code générique, éliminent les composants d’enveloppe et facilitent grandement l’utilisation de TypeScript, c’est pourquoi ils sont la norme. Les composants fonctionnant avec connect() n’ont pas besoin d’être réécrits ; convertissez-les lorsque vous y touchez de toute façon.

La différence entre égalité et identité en une ligne

Gardez cette distinction à l’esprit :

useSelector()
      ↓
=== reference equality

contre

connect()
      ↓
shallow equality

De nombreux bugs du type « ça fonctionnait avant le refactoring » proviennent de cela : un objet vierge était sécurisé dans mapStateToProps, mais échoue avec === dans useSelector().

Typage de React-Redux avec TypeScript

React-Redux fournit ses propres définitions de types, et la documentation décrit une configuration typée standard. Elle repose sur six noms principaux :

RootState
AppDispatch
AppStore
useAppSelector
useAppDispatch
useAppStore

RootState : déduire le type d’état à partir du store

Étant donné un magasin configuré avec Redux Toolkit,

const store = configureStore({
  reducer: {
    counter: counterReducer,
    users: usersReducer
  }
})

on déduit le type d’état à partir de ce que getState renvoie :

export type RootState =
  ReturnType<typeof store.getState>

Cela évite d’avoir à définir manuellement la structure,

type RootState = {
  counter: CounterState
  users: UsersState
}

car le type déduit suit automatiquement les reducers.

AppDispatch : le type de dispatch incluant le middleware

On déduit le type de dispatch de la même manière :

export type AppDispatch =
  typeof store.dispatch

Le type simple Dispatch de Redux ne connaît que des actions simples ; le type déduit prend en compte votre middleware, ce qui est important lorsque vous utilisez :

  • un middleware qui modifie ce que dispatch accepte
  • des thunks
  • un dispatch personnalisé
  • des actions asynchrones de n’importe quel type

Sans AppDispatch, l’envoi d’un thunk provoque une erreur de type.

AppStore : le type du magasin lui-même

Le type du magasin ne demande qu’une autre déduction :

export type AppStore =
  typeof store

Ainsi, chaque aspect possède une seule source de vérité. Le type d’état :

RootState
    ↓
state type

Le type de dispatch :

AppDispatch
    ↓
dispatch type

Le type de store :

AppStore
    ↓
store type

AppStore devient particulièrement utile lorsque vous créez un store par requête ou par test et que vous devez le transmettre.

Hooks pré-typés avec withTypes()

Dès React-Redux 9.1.0, chaque hook expose une méthode .withTypes() :

useDispatch.withTypes()
useSelector.withTypes()
useStore.withTypes()

Le schéma documenté permet de créer des hooks spécifiques à l’application à partir d’eux une seule fois :

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Ce que les hooks typés vous apportent

Sans eux, chaque sélecteur nécessite une annotation explicite :

const user = useSelector(
  (state: RootState) =>
    state.auth.user
)

Avec le hook typé,

const user = useAppSelector(
  state => state.auth.user
)

le compilateur sait déjà

state = RootState

Le côté dispatch fonctionne de la même manière :

const dispatch = useAppDispatch()

Ce dispatch accepte des thunks ainsi que tout autre élément autorisé par votre middleware, avec une vérification approfondie.

Fichier hooks.ts pour l’application

Dans un projet typique, ces éléments sont regroupés dans un petit module :

import {
  useDispatch,
  useSelector,
  useStore
} from 'react-redux'

import type {
  RootState,
  AppDispatch,
  AppStore
} from './store'

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Les composants importent depuis ce module plutôt que directement depuis react-redux :

const user = useAppSelector(
  state => state.auth.user
)

const dispatch = useAppDispatch()

ConnectedProps pour du code connect() typé

Les bases de code qui typent les composants connect() contiendront

ConnectedProps

Divisez l’appel à connect en un connecteur d’abord, puis extrayez les props qu’il injecte :

const connector = connect(
  mapState,
  mapDispatch
)

type PropsFromRedux =
  ConnectedProps<typeof connector>

PropsFromRedux décrit précisément ce que le connecteur injecte, de sorte que les types mappés ne sont jamais dupliqués.

Conception de bons sélecteurs

Il est tentant de considérer un sélecteur comme rien de plus que

state => state.user

Dans une application plus grande, les sélecteurs doivent être considérés comme des limites qui transforment le format de stockage de l’état en la forme souhaitée par l’interface utilisateur :

Redux State
     ↓
Selector
     ↓
UI-friendly data

Par exemple, la règle déterminant quels éléments sont visibles peut être contenue dans une fonction nommée :

const selectVisibleTodos =
  state =>
    state.todos.filter(
      todo => !todo.hidden
    )

et le composant se contente de demander le résultat :

const todos = useSelector(
  selectVisibleTodos
)

Le composant reste de nature présentationnelle, tandis que la règle peut être testée indépendamment.

Un bon sélecteur est précis

const selectUserName =
  state => state.auth.user.name

Il renvoie exactement ce que le composant affiche et rien de plus, ce qui ne déclenche une mise à jour que lorsque ce nom change.

Sélectionner tout l’état est presque toujours une erreur

const selectEverything =
  state => state

Redux génère un nouvel objet d’état racine chaque fois qu’un réducteur modifie quelque chose. Un sélecteur qui renvoie la racine produit donc une nouvelle référence après pratiquement chaque action :

Anything in Redux changes
        ↓
Root state reference changes
        ↓
Selector result changes
        ↓
Component re-renders

Les vérifications de développement de React-Redux détectent ce schéma.

Gardez les sélecteurs spécifiques

Préférez plusieurs lectures ciblées :

const count =
  useSelector(
    state => state.counter.value
  )

const user =
  useSelector(
    state => state.auth.currentUser
  )

plutôt qu’une seule lecture globale :

const state =
  useSelector(state => state)

Une règle pratique :

Sélectionnez la plus petite partie de l’état que le composant peut réellement utiliser.

Vérifications des sélecteurs en mode développement

Les versions récentes de React-Redux effectuent des vérifications supplémentaires sur vos sélecteurs lors des builds de développement. Deux d’entre elles méritent d’être connues par leur nom.

La vérification de stabilité

La première vérification appelle votre sélecteur une seconde fois avec le même état et compare les résultats :

selector(state)
      ↓
run again with same state
      ↓
same result?

Si le résultat est

same reference

le sélecteur est stable. Sinon

new reference

React-Redux affiche un avertissement, car un sélecteur qui renvoie une nouvelle référence pour des entrées identiques rérendera son composant à chaque mise à jour du store.

Un exemple typique est le sélecteur en objet littéral mentionné précédemment :

const data = useSelector(
  state => ({
    count: state.count,
    user: state.user
  })
)

Puisque l’objet est reconstruit à chaque appel, la vérification constate :

same input
    ↓
different object
    ↓
unstable selector

Configuration de la fréquence des vérifications

Vous pouvez définir cette fréquence pour toute l’application via le Provider :

<Provider
  store={store}
  stabilityCheck="always"
>
  <App />
</Provider>

ou la surcharger pour un appel spécifique d’un hook :

const count = useSelector(
  selectCount,
  {
    devModeChecks: {
      stabilityCheck: 'once'
    }
  }
)

Les valeurs acceptées sont :

never
once
always

La valeur par défaut est 'once', ce qui signifie que la vérification a lieu lors du premier appel de chaque hook. Rien de tout cela n’est exécuté dans les versions en production.

Vérification de la fonction d’identité

La deuxième vérification cherche un sélecteur qui renvoie ses entrées inchangées :

state => state

Dans un composant, cela ressemble à ceci :

const state = useSelector(
  state => state
)

Cela lie le composant à chaque modification du store :

Any Redux change
       ↓
Root state changes
       ↓
Component re-renders

La documentation qualifie cela de vérification de la fonction d’identité. Dans les versions antérieures, on l’appelait noopCheck, nom que vous pouvez encore trouver dans des configurations anciennes.

La solution reste la même qu’auparavant : remplacer

const state = useSelector(
  state => state
)

par des lectures des valeurs spécifiques dont vous avez besoin :

const count = useSelector(
  state => state.counter.value
)

const user = useSelector(
  state => state.auth.currentUser
)

Rendu et performances au-delà des sélecteurs

Le rendu des parents continue de se propager

useSelector() ne gère que les rendus causés par des mises à jour du store. Il n’affecte pas la règle normale de React selon laquelle un composant se rend quand son parent se rend :

Parent renders
      ↓
Child renders

Cela se produit même si aucun état Redux n’a changé du tout. Lorsqu’un enfant est gourmand en ressources et que ses props sont stables, enveloppez-le dans

React.memo()

Cela diffère de connect(), dont l’encapsulation se comporte comme un composant mémorisé ; les composants basés sur des hooks ne bénéficient pas gratuitement d’un tel comportement.

Combinaison de React.memo et useSelector

Ici, le composant s’abonne à un compteur et est également mémorisé en fonction de sa propriété name:

const Counter = ({ name }) => {
  const count = useSelector(
    state => state.counter.value
  )

return (
    <div>
      {name}: {count}
    </div>
  )
}
export default React.memo(Counter)

Ces deux mécanismes couvrent les deux sources de rendu :

Redux selector
       +
React.memo
       ↓
More controlled rendering

La mémorisation a ses propres coûts, il faut donc d’abord effectuer un profilage. Pour une liste plus complète des déclencheurs de rendu, consultez notre guide sur les patterns courants causant des re-rendus inutiles dans React.

Un modèle de performance qui tient sur un écran

Après chaque action, React-Redux réexécute les sélecteurs des composants abonnés, compare chaque résultat avec le précédent et affiche uniquement les composants dont les résultats diffèrent :

Redux action
     ↓
Store updates
     ↓
Selectors execute
     ↓
Selector results compared
     ↓
Changed?
 ┌───┴────┐
No       Yes
│         │
│         ▼
│      Re-render
│
└── No Redux-triggered render

Votre tâche consiste donc à écrire des sélecteurs qui :

  1. retournent uniquement les données utilisées par le composant
  2. renvoient des références stables lorsque rien de pertinent n’a changé
  3. n’allouent pas de nouveaux objets ou tableaux inutilement
  4. mémoisent les données dérivées dont le calcul est coûteux

Règle un : ne jamais sélectionner l’état racine

useSelector(state => state)

Règle deux : éviter de créer des objets d’encapsulation dans les sélecteurs

useSelector(state => ({
  user: state.user,
  count: state.count
}))

Ne retournez un tel objet que si vous le combinez délibérément avec

shallowEqual

ou si vous le passez à travers un sélecteur mémoisé.

Règle trois : mémoiser les calculs coûteux

Le tri, le filtrage, le regroupement et la jointure appartiennent à

createSelector(...)

Règle quatre : appliquer React.memo sur les éléments pertinents

Au préalable de mettre en mémoire un composant, suivez une courte liste de vérification :

Is the component expensive?
       ↓
Does it receive stable props?
       ↓
Does it re-render unnecessarily?
       ↓
Then consider React.memo()

Si votre base de code utilise le React Compiler, une grande partie de cette mise en mémoire manuelle est probablement déjà gérée.

Règle cinq : choisir le plus précisément possible selon les possibilités du composant

Trop large :

state => state

Mieux :

state => state.auth.user
state => state.auth.user.name

Cas limites rares : props obsolètes et enfants zombies

La plupart des applications ne rencontrent jamais ces deux problèmes, mais ils expliquent pourquoi les sélecteurs défensifs sont une bonne pratique.

Props obsolètes

Un problème de props obsolètes nécessite un sélecteur qui dépend d’une propriété, ainsi qu’une mise à jour du stockage qui modifie à la fois l’état et, indirectement, cette propriété :

Selector depends on component props
        ↓
Redux action updates state
        ↓
Parent would receive new props
        ↓
Child selector runs first
        ↓
Selector sees old props

La souscription de l’élément enfant peut être déclenchée avant que le parent n’ait réaffiché l’interface et transmis la nouvelle propriété. Pendant un instant, le sélecteur combine un état mis à jour avec une ancienne propriété. Une forme typique est :

const todo = useSelector(
  state => state.todos[props.id]
)

Si l’élément ayant ce id vient d’être supprimé, ou si le parent est sur le point de transmettre un id différent, le sélecteur lit temporairement des données qui ne correspondent plus.

Rédaction de sélecteurs tolérants aux données manquantes

La version fragile suppose que l’élément existe toujours :

state.todos[props.id].name

Une version défensive cherche d’abord l’élément,

const todo =
  state.todos[props.id]

et ne lit dessus qu’ensuite :

return todo
  ? todo.name
  : undefined

La décision repose sur une simple vérification :

Does data exist?
      ↓
Yes → use it
No  → handle safely

La chaînage optionnel (state.todos[id]?.name) exprime la même idée en une seule expression.

Enfants zombies

Le scénario du enfant-zombie implique un parent qui affiche une liste et un enfant qui s’abonne à l’un de ses éléments :

Parent
  │
  └── Child

La séquence se déroule comme suit :

  1. L’enfant s’abonne au store.
  2. Une action supprime les données affichées par l’enfant.
  3. Le parent cessera d’afficher cet enfant lors de sa prochaine mise à jour.
  4. Au préalable, la souscription de l’enfant est exécutée.
  5. Le sélecteur de l’enfant tente d’accéder à des données qui ont déjà disparu.

Un sélecteur non protégé provoque une erreur à ce dernier étape. React-Redux dispose de mécanismes pour gérer les erreurs de sélecteur causées par les mises à jour du store, mais les sélecteurs défensifs restent la solution la plus fiable.

Pourquoi les hooks sont plus vulnérables que connect

connect() crée un arbre d’abonnement imbriqué : chaque composant connecté n’est mis à jour qu’après que ses ancêtres connectés l’aient été, ce qui assure un ordre top-down. Les hooks s’attachent directement au store sans cette hiérarchie, de sorte que les garanties d’ordre sont plus faibles et ces cas limites deviennent théoriquement possibles.

Considérez cela comme une connaissance de base, et non comme une raison d’éviter les hooks. La position officielle est que ces problèmes sont rares dans les applications réelles.

Fonctionnalités avancées de Provider

Un contexte personnalisé pour des stores isolés

Par défaut,

<Provider store={store}>

le store est publié via le contexte intégré de React-Redux. Une bibliothèque de composants réutilisables qui utilise Redux en interne peut entrer en conflit avec le store de l’application hôte de cette manière. Pour éviter cela, Provider accepte un contexte que vous avez défini vous-même :

<Provider
  context={MyContext}
  store={myStore}
>

Les hooks liés à ce contexte proviennent de fonctions factory :

createStoreHook()
createDispatchHook()
createSelectorHook()

Cela est principalement important pour les bibliothèques réutilisables où plusieurs stores risqueraient sinon de entrer en conflit.

batch() et le regroupement automatique de React 18

Le code plus ancien enveloppe souvent des appels dispatch consécutifs dans batch() afin que React se rende une seule fois au lieu de deux :

batch(() => {
  dispatch(action1())
  dispatch(action2())
})

React 18 regroupe automatiquement les mises à jour, y compris dans les promesses et les timeouts, de sorte qu’une application React 18 typique n’a pas besoin de batch() à cette fin. Les dernières versions de React-Redux le conservent principalement pour des raisons de compatibilité ; consultez la documentation actuelle et attendez-vous à le trouver dans du code plus ancien.

serverState pour le SSR et l’hydratation

Pour le rendu côté serveur, Provider prend une propriété supplémentaire :

<Provider
  store={store}
  serverState={preloadedState}
>

Le serveur génère de l’HTML à partir d’un état initial et envoie cet état au navigateur. serverState permet à l’affichage après hydratation d’utiliser le même snapshot, évitant ainsi tout désaccord :

Server
  ↓
Initial Redux State
  ↓
HTML
  ↓
Browser Hydration
  ↓
Provider(serverState)
  ↓
Consistent initial render

Une structure de projet typique

src/
│
├── app/
│   ├── store.ts
│   └── hooks.ts
│
├── features/
│   │
│   ├── counter/
│   │   ├── counterSlice.ts
│   │   └── Counter.tsx
│   │
│   ├── users/
│   │   ├── usersSlice.ts
│   │   └── Users.tsx
│   │
│   └── auth/
│       ├── authSlice.ts
│       └── Login.tsx
│
├── App.tsx
└── main.tsx

store.ts

const store = configureStore({
  reducer: {
    counter: counterReducer,
    users: usersReducer,
    auth: authReducer
  }
})

export type RootState =
  ReturnType<typeof store.getState>

export type AppDispatch =
  typeof store.dispatch

export type AppStore =
  typeof store

hooks.ts

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Un composant qui utilise les deux

function Counter() {
  const count = useAppSelector(
    state => state.counter.value
  )

const dispatch = useAppDispatch()
  return (
    <>
      <span>{count}</span>
      <button
        onClick={() =>
          dispatch(increment())
        }
      >
        +
      </button>
    </>
  )
}

Component
    │
    ├── useAppSelector()
    │        ↓
    │      READ
    │
    └── useAppDispatch()
             ↓
          DISPATCH
             ↓
           Redux
             ↓
          New State
             ↓
       useAppSelector()
             ↓
          Component

Où s’intègre Redux Toolkit

Redux Toolkit et React-Redux sont complémentaires, pas des alternatives :

Redux Toolkit
      +
React-Redux

Redux Toolkit améliore la partie Redux : configuration du store, réducteurs, logique asynchrone, sélecteurs mémorisés et récupération de données.

configureStore
createSlice
createAsyncThunk
createSelector
RTK Query

React-Redux reste responsable de la connexion à React :

Provider
useSelector
useDispatch
useStore
connect

Le guide de démarrage officiel React-Redux configure les deux ensemble, et cette combinaison est la par défaut pour de nouveaux projets.

Le flux complet en un seul diagramme

En rassemblant toutes les composantes, du Provider jusqu’à la vérification d’égalité :

React
                   │
                   ▼
             <Provider>
                   │
                   ▼
             Redux Store
                   │
          ┌────────┴────────┐
          │                 │
    useSelector()      useDispatch()
          │                 │
          │                 ▼
          │               Action
          │                 │
          │                 ▼
          │              Reducer
          │                 │
          │                 ▼
          │             New State
          │                 │
          └─────────┬───────┘
                    ▼
              Selector runs
                    │
                    ▼
             Equality check
                    │
             ┌──────┴──────┐
             │             │
           Same         Different
             │             │
             ▼             ▼
         No Redux       Re-render
          render

Erreurs courantes et moyens de les détecter

S’abonner à l’état complet

useSelector(state => state)

Remplacez cela par des sélecteurs spécifiques.

Envelopper les valeurs dans un nouvel objet

useSelector(state => ({
  user: state.user
}))

Divisez-le en hooks distincts, ou ajoutez délibérément shallowEqual.

Filtrage ou mappage à l’intérieur du sélecteur à chaque exécution

useSelector(state =>
  state.todos.filter(...)
)

Si cette opération s’exécute fréquemment ou sur de grandes listes, placez-la dans un sélecteur mémorisé.

Lecture des données de rendu via useStore

Appeler

store.getState()

dans la logique de rendu vous donne une valeur sans abonnement. Utilisez

useSelector()

afin que le composant soit mis à jour lorsque la valeur change.

Mutation directe de l’état

state.user.name = 'John'

brise le modèle d’actualisation immuable de Redux : les références ne changent pas, donc les sélecteurs détectent « aucune modification » et les composants ne s’affichent pas. (À l’intérieur des réducteurs createSlice de Redux Toolkit, ce style est autorisé car Immer le transforme en une actualisation immuable ; ailleurs, c’est une erreur.)

Effets secondaires à l’intérieur des sélecteurs

state => {
  fetch(...)
  return state.user
}

Un sélecteur doit calculer et retourner, rien de plus.

Mémorisation par réflexe

Disperser cela partout

useMemo()
useCallback()
React.memo()

ajoute de la complexité et des coûts de comparaison sans preuve qu’il y a un bénéfice. Optimisez le rendu que vous avez mesuré.

Mauvaise localisation des instances de sélecteur mémorisées

global
per component
per component instance

Une instance partagée utilisée avec de nombreux arguments peut ne jamais accéder à son cache.

Questions d’entretien avec des réponses courtes

Qu’est-ce que React-Redux, et pourquoi le Provider est-il nécessaire ?

C’est le lien officiel entre React et Redux : Provider, les hooks ainsi que connect permettent aux composants de lire l’état, de réagir aux changements et d’envoyer des actions. Le Provider place le store dans le contexte React afin que tout composant descendant puisse y accéder.

useSelector contre useDispatch ?

L’un permet de lire :

useSelector
    ↓
READ Redux state

Les autres écrivent :

useDispatch
    ↓
DISPATCH Redux actions

Comment useSelector déclenche-t-il un re-render ?

Après chaque action, il exécute à nouveau le sélecteur et compare le résultat avec le précédent en utilisant === ou la fonction d’égalité que vous avez fournie. Seule une différence provoque un rendu.

Pourquoi ce sélecteur provoque-t-il constamment un re-render, et comment le corriger ?

useSelector(state => ({
  user: state.user,
  count: state.count
}))

Il crée un nouvel objet à chaque exécution, ce qui fait que la vérification de référence échoue toujours. Les trois solutions :

1. Multiple useSelector calls

2. shallowEqual

3. Memoized selector

useSelector contre connect ?

Les hooks comparent les résultats des sélecteurs par référence ; connect() effectue une comparaison superficielle des props provenant de mapStateToProps. Les hooks sont la solution par défaut, tandis que connect() reste pris en charge.

Pourquoi des sélecteurs mémorisés ?

Ils recomptent les données dérivées uniquement lorsque les entrées changent, sinon ils retournent la même référence. Un filtre en est un exemple classique :

todos
  ↓
filter completed
  ↓
new array

Qu’est-ce que RootState, AppDispatch et withTypes() ?

Le type inféré de l’ensemble de l’état :

type RootState =
  ReturnType<typeof store.getState>

Le type d’envoi inféré, y compris les middleware tels que les thunks :

type AppDispatch =
  typeof store.dispatch

Et les outils qui retournent des hooks pré-configurés pour ces types :

useDispatch.withTypes<AppDispatch>()

useSelector.withTypes<RootState>()

useStore.withTypes<AppStore>()

Pourquoi des hooks typés ?

Ils remplacent les annotations répétées comme

useSelector(
  (state: RootState) =>
    state.user
)

par

useAppSelector(
  state => state.user
)

Pourquoi les sélecteurs doivent-ils être purs ?

Un sélecteur peut être exécuté plusieurs fois pour le même état, pendant le rendu, après chaque action, et à des moments que votre code ne contrôle pas. Tout effet secondaire serait exécuté un nombre imprévisible de fois.

À quoi sert useStore ?

Des cas rares qui nécessitent réellement l’objet store. La lecture de l’état pour le rendu s’effectue via useSelector().

Qu’est-ce que les enfants zombies et les props obsolètes ?

Ce sont tous deux des problèmes de tri rares. Un enfant zombie traite une mise à jour avant que son parent ne le désinstalle et lit des données supprimées ; les props obsolètes signifient qu’un sélecteur dépendant de props s’exécute avec une état à jour mais des props anciens. Les sélecteurs défensifs gèrent les deux cas.

Le batch() est-il encore nécessaire avec React 18 ?

Généralement non, car React 18 effectue automatiquement le regroupement des opérations, mais on peut encore le rencontrer dans du code plus ancien.

Pourquoi connect et les hooks peuvent-ils afficher des résultats différents ?

Des modèles de souscription différents et des comparaisons distinctes :

connect()
    ↓
shallow equality

contre

useSelector()
    ↓
strict === equality

Pourquoi useSelector s’exécute-t-il si souvent ?

Il :

  1. s’exécute pendant le rendu
  2. écoute l’objet store
  • se réexécute après chaque action envoyée
  • reutilise son résultat en mémoire pendant le rendu uniquement lorsque la fonction de sélection et l’état restent inchangés
  • Ainsi, un sélecteur en ligne, étant une nouvelle fonction à chaque rendu, s’exécute à chaque fois ; une référence de sélecteur stable permet à React-Redux d’éviter cet appel.

    Qu’étudier, par ordre de priorité

    Niveau un : à maîtriser absolument

    Provider
    useSelector
    useDispatch
    Redux Store flow
    Selectors
    === equality
    Re-render behavior
    Redux Toolkit + React-Redux
    TypeScript
    RootState
    AppDispatch
    .withTypes()
    

    Niveau deux : connaissances solides et fonctionnelles

    shallowEqual
    Memoized selectors
    createSelector
    useStore
    connect
    mapStateToProps
    mapDispatchToProps
    ConnectedProps
    React.memo
    

    Niveau trois : sujets avancés

    Stale props
    Zombie children
    Selector + props
    Selector memoization
    Custom context
    Development mode checks
    SSR serverState
    batch()
    

    Quoi ne pas mémoriser

    Il n’est pas nécessaire d’apprendre la documentation ligne par ligne. Il est acceptable de sauter :

    • la manière dont le mécanisme d’abonnement est implémenté en interne
    • les options de connect() peu utilisées
    • les patterns obsolètes depuis longtemps
    • les détails d’implémentation du code source
  • le texte exact de chaque avertissement de développement
  • chaque propriété avancée de Provider
  • Ce qui importe, c’est la logique derrière les règles. Connaître ce fait

    useSelector uses ===
    

    est moins utile que de pouvoir y répondre

    Why?
    

    à savoir :

    Because returning a new object
    creates a new reference.
    

    cela mène à la chaîne

    New reference
         ↓
    === false
         ↓
    re-render
    

    Si vous comprenez cette chaîne, vous pouvez déduire la plupart des autres règles par vous-même.

    Dix règles à respecter

    1. Lire avec useSelector

    useSelector()
    

    c’est ainsi que les composants lisent l’état.

    2. Écrire avec useDispatch

    useDispatch()
    

    c’est ainsi que les composants envoient des actions.

    3. Envelopper l’application dans Provider

    <Provider store={store}>
    

    4. Se souvenir des règles de comparaison

    useSelector → ===
    connect → shallow comparison
    

    5. Ne jamais sélectionner l’état racine

    useSelector(state => state)
    

    6. Faire attention aux sélecteurs qui retournent des objets

    useSelector(state => ({
      ...
    }))
    

    7. Mémoriser les données dérivées coûteuses

    Memoized selector
    

    8. Maintenir la discipline des sélecteurs

    Pure
    Predictable
    Granular
    

    9. Définir d’abord le store, puis les hooks

    D’abord les types déduits :

    RootState
    AppDispatch
    AppStore
    

    puis les hooks d’application construits à partir d’eux :

    useAppSelector
    useAppDispatch
    useAppStore
    

    10. Utiliser la combinaison moderne

    Redux Toolkit
           +
    React-Redux Hooks
    

    Toute l’API en une seule page

    Une référence compacte couvrant les hooks principaux, les bonnes pratiques de performance, les types TypeScript, l’API legacy ainsi que les sujets avancés :

    ┌───────────────────────────────────────────────┐
    │               REACT-REDUX                     │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ <Provider store={store}>                      │
    │        ↓                                      │
    │ Makes Redux available to React                │
    │                                               │
    │ useSelector()                                 │
    │        ↓                                      │
    │ READ state                                    │
    │        ↓                                      │
    │ Default comparison: ===                       │
    │                                               │
    │ useDispatch()                                 │
    │        ↓                                      │
    │ DISPATCH actions                              │
    │                                               │
    │ useStore()                                    │
    │        ↓                                      │
    │ Direct store access                           │
    │        ↓                                      │
    │ Use rarely                                    │
    │                                               │
    ├───────────────────────────────────────────────┤
    │ PERFORMANCE                                   │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ Avoid state => state                          │
    │ Avoid unnecessary object creation             │
    │ Use granular selectors                        │
    │ Use shallowEqual when appropriate             │
    │ Use memoized selectors for derived data       │
    │ Use React.memo when justified                 │
    │                                               │
    ├───────────────────────────────────────────────┤
    │ TYPESCRIPT                                    │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ RootState = ReturnType<typeof store.getState> │
    │ AppDispatch = typeof store.dispatch           │
    │ AppStore = typeof store                       │
    │                                               │
    │ useAppSelector                                │
    │ useAppDispatch                                │
    │ useAppStore                                   │
    │                                               │
    ├───────────────────────────────────────────────┤
    │ LEGACY / EXISTING APPLICATIONS                │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ connect()                                     │
    │ mapStateToProps                               │
    │ mapDispatchToProps                            │
    │ ConnectedProps                                │
    │                                               │
    ├───────────────────────────────────────────────┤
    │ ADVANCED                                      │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ Stale Props                                   │
    │ Zombie Children                               │
    │ Custom Context                                │
    │ SSR serverState                               │
    │ Development checks                            │
    │ batch()                                       │
    │                                               │
    └───────────────────────────────────────────────┘
    

    Et le cycle, représenté une fois de plus avec les chemins de lecture et d’écriture côte à côte :

                            REACT
                               │
                               │
                        <Provider />
                               │
                               ▼
                        ┌─────────────┐
                        │ Redux Store │
                        └──────┬──────┘
                               │
                   ┌───────────┴───────────┐
                   │                       │
                   ▼                       ▲
            useSelector()             useDispatch()
                   │                       │
                   │                       │
                 READ                    ACTION
                   │                       │
                   │                       │
                   │                  ┌────┴─────┐
                   │                  │ Reducer  │
                   │                  └────┬─────┘
                   │                       │
                   │                       ▼
                   │                  New State
                   │                       │
                   └───────────────────────┘
                               │
                               ▼
                        Equality Check
                               │
                        ┌──────┴──────┐
                        │             │
                      Same         Changed
                        │             │
                        ▼             ▼
                    No Redux      Re-render
                     render
    

    Pour un nouveau projet TypeScript, la configuration recommandée se résume à cette structure :

                Redux Toolkit
                       +
                  React-Redux
                       │
            ┌──────────┴──────────┐
            │                     │
         Store                 Provider
            │                     │
            │               Application tree
            │                     │
            └──────────┬──────────┘
                       │
              ┌────────┴────────┐
              │                 │
       useAppSelector()   useAppDispatch()
              │                 │
            READ              WRITE
              │                 │
              └────────┬────────┘
                       │
                    Redux
                       │
                  New State
                       │
                    Selector
                       │
                    Re-render
    

    En conclusion

    React-Redux devient beaucoup plus simple dès lors que l’on cesse de considérer ses exports comme des outils indépendants. Cette liste de noms

    Provider
    useSelector
    useDispatch
    connect
    shallowEqual
    createSelector
    useStore
    

    Décrit un seul pipeline avec une tâche par étape. Le fournisseur expose le stockage :

    Provider
       ↓
    makes Store available
    

    L’hook sélecteur lit :

    useSelector
       ↓
    reads selected state
    

    L’hook de dispatch envoie :

    useDispatch
       ↓
    sends actions
    

    Les réducteurs génèrent l’état suivant :

    Reducer
       ↓
    creates new state
    

    Les sélecteurs le façonnent pour l’interface utilisateur :

    Selector
       ↓
    derives data
    

    Vérification d’égalité pour déterminer s’il y a eu des changements importants :

    Equality
       ↓
    decides whether selected data changed
    

    Et React s’occupe du rendu :

    React
       ↓
    re-renders when necessary
    

    Du côté TypeScript, la chaîne est tout aussi linéaire :

    Store
     ↓
    RootState
    AppDispatch
    AppStore
     ↓
    .withTypes()
     ↓
    useAppSelector()
    useAppDispatch()
    useAppStore()
    

    Lorsqu’un composant affiche plus qu’il ne le devrait, il faut se poser les mêmes quatre questions à chaque fois :

    useSelector()
          ↓
    What does my selector return?
          ↓
    Is the reference stable?
          ↓
    Does the selected value actually change?
          ↓
    Should this component re-render?
    

    Points clés :

    • La plupart des problèmes de performance avec React-Redux proviennent de sélecteurs qui retournent de nouvelles références, et non du Redux lui-même.
  • useSelector() effectue une comparaison avec === ; connect() effectue une comparaison superficielle. Transposer du code d’un à l’autre sans ajuster les sélecteurs modifie le comportement.
  • Sélectionnez de manière ciblée, mémorisez les données dérivées à l’aide d’instances createSelector au niveau du module, et utilisez shallowEqual uniquement lorsque le résultat objet est intentionnel.
  • Déduisez RootState, AppDispatch et AppStore à partir du store et créez des hooks typés avec .withTypes().
  • Les props obsolètes, les enfants zombies, les contextes personnalisés, batch() et serverState méritent d’être compris, mais ce sont des cas limites et non des problèmes courants.
  • Comme auto-évaluation, essayez d’expliquer de mémoire chaque titre de ce guide, depuis Provider et l’égalité en passant par les hooks typés jusqu’à serverState, et d’écrire la configuration de base sans consulter quoi que ce soit.

    Pour plus de détails, les références officielles sont l’API des hooks, l’API de Provider, le guide d’utilisation avec TypeScript, le Début rapide ainsi que la référence sur connect(). Étudiez d’abord le cycle de base et l’égalité, puis les sélecteurs, TypeScript, les performances, connect et les cas limites, afin que les détails de l’API aient un modèle auquel s’appuyer.

    Lectures complémentaires

  • Prop Drilling N’est Pas Une Raison d’Installer Redux ou Zustand — Étudiez quatre raisons courantes de l’ajout d’une bibliothèque de gestion d’état à du code React fonctionnel : le Context pour le prop drilling, useState, useSyncExternalStore, ainsi que le coût de re-render du Context.
  • Le Schedulateur de React et la Boucle d’Événements : Qui Décide Vraiment de Quand le Travail Se Exécute — Découvrez comment le schedulateur coopératif de React s’exécute au sein de la boucle d’événements JavaScript, pourquoi les transitions peuvent être différées, et pourquoi aucun schedulateur ne peut sauver un thread bloqué.
  • Réflexion sur les React Hooks à travers les souvenirs de rendu et les compromis — Un modèle mental de niveau avancé pour les Hooks React et React Native : souvenirs de rendu, effets, refs, mémorisation, Hooks personnalisés, ainsi que la manière d’expliquer les compromis lors des entretiens.