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.
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 :
- lit l’état actuel du store
- appelle votre sélecteur avec cet état
- conserve la valeur retournée comme le résultat « dernièrement sélectionné »
- enregistre une abonnement au store pour ce composant
- réexécute le sélecteur après chaque action envoyée
- compare le résultat précédent avec le nouveau
- 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
dispatchaccepte - 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 :
- retournent uniquement les données utilisées par le composant
- renvoient des références stables lorsque rien de pertinent n’a changé
- n’allouent pas de nouveaux objets ou tableaux inutilement
- 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 :
- L’enfant s’abonne au store.
- Une action supprime les données affichées par l’enfant.
- Le parent cessera d’afficher cet enfant lors de sa prochaine mise à jour.
- Au préalable, la souscription de l’enfant est exécutée.
- 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 :
- s’exécute pendant le rendu
- écoute l’objet store
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
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.createSelector au niveau du module, et utilisez shallowEqual uniquement lorsque le résultat objet est intentionnel.RootState, AppDispatch et AppStore à partir du store et créez des hooks typés avec .withTypes().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
- React Query et Redux : Reconsidérer l’état du serveur dans les grandes applications — Découvrez pourquoi une application de chat en production a utilisé TanStack Query plutôt que Redux pour gérer les données du serveur, et où Redux trouve encore sa place dans l’architecture React moderne.
- Structurer une couche de données TanStack Query, de queryOptions à Rollbacks — Construisez pas à pas une couche de données TanStack Query : queryOptions partagés, générateurs de clés, sélecteurs, pagination, préchargement, invalidation centrale et mises à jour optimistes sécurisées.