Du traducteur:
Je présente une traduction gratuite d'un article sur la façon de mettre en œuvre une solution efficace pour remplacer Redux par le contexte React et les hooks. L'indication d'erreurs de traduction ou de texte est la bienvenue. Profitez de votre visionnage.
Depuis la sortie de la nouvelle API contextuelle dans React 16.3.0, de nombreuses personnes se sont demandé si la nouvelle API est suffisamment bonne pour la considérer comme un remplacement de Redux? Je pensais la même chose, mais je ne comprenais pas complètement même après la sortie de la version 16.8.0 avec crochets. J'essaie d'utiliser des technologies populaires, le chemin n'est pas toujours de comprendre toute la gamme des problèmes qu'elles résolvent, donc je suis trop habitué à Redux.
Et c'est ainsi que je me suis inscrit à
la newsletter de
Kent C. Dodds et que j'ai découvert quelques courriels sur le sujet de la gestion du contexte et de l'état. J'ai commencé à lire .... et lisez ... et après 5 articles de blog, quelque chose a cliqué.
Pour comprendre tous les concepts de base derrière cela, nous allons créer un bouton, en cliquant sur lequel nous recevrons des blagues avec
icanhazdadjoke et les afficherons. Ceci est un exemple petit mais suffisant.
Pour se préparer, commençons par deux conseils apparemment aléatoires.
Tout d'abord, permettez-moi de vous présenter mon ami
console.count
:
console.count('Button')
Nous allons ajouter un appel
console.count
à chaque composant pour voir combien de fois il est rendu. Assez cool, hein?
Deuxièmement, lorsqu'un composant React est restitué, il
ne restitue pas le contenu transmis en tant
children
.
function Parent({ children }) { const [count, setCount] = React.useState(0) console.count('Parent') return ( <div> <button type="button" onClick={() => { setCount(count => count + 1) }}> Force re-render </button> {children} </div> ) } function Child() { console.count('Child') return <div /> } function App() { return ( <Parent> <Child /> </Parent> ) }
Après quelques clics sur le bouton, vous devriez voir le contenu suivant dans la console:
Parent: 1 Child: 1 Parent: 2 Parent: 3 Parent: 4
Gardez à l'esprit qu'il s'agit d'un moyen souvent négligé d'améliorer les performances de votre application.
Maintenant que nous sommes prêts, créons le squelette de notre application:
import React from 'react' function Button() { console.count('Button') return ( <button type="button"> Fetch dad joke </button> ) } function DadJoke() { console.count('DadJoke') return ( <p>Fetched dad joke</p> ) } function App() { console.count('App') return ( <div> <Button /> <DadJoke /> </div> ) } export default App
Button
devrait recevoir un générateur d'actions (environ Action Creator. La traduction est tirée de la
documentation Redux en russe ) qui recevra une blague.
DadJoke
doit obtenir l'état et l'
App
afficher les deux composants à l'aide du contexte du fournisseur.
Créez maintenant un composant personnalisé et appelez-le
DadJokeProvider
qui, à l'intérieur, gérera l'état et encapsulera les composants enfants dans le fournisseur de contexte. N'oubliez pas que la mise à jour de son état ne restituera pas l'intégralité de l'application en raison de l'optimisation des enfants ci-dessus dans React.
Alors, créez un fichier et appelez-le
contexts/dad-joke.js
:
import React from 'react' const DadJokeContext = React.createContext() export function DadJokeContextProvider({ children }) { const state = { dadJoke: null } const actions = { fetchDadJoke: () => {}, } return ( <DadJokeContext.Provider value={{ state, actions }}> {children} </DadJokeContext.Provider> ) }
Nous exportons également 2 hooks pour obtenir la valeur du contexte.
export function useDadJokeState() { return React.useContext(DadJokeContext).state } export function useDadJokeActions() { return React.useContext(DadJokeContext).actions }
Maintenant, nous pouvons implémenter ceci:
import React from 'react' import { DadJokeProvider, useDadJokeState, useDadJokeActions, } from './contexts/dad-joke' function Button() { const { fetchDadJoke } = useDadJokeActions() console.count('Button') return ( <button type="button" onClick={fetchDadJoke}> Fetch dad joke </button> ) } function DadJoke() { const { dadJoke } = useDadJokeState() console.count('DadJoke') return ( <p>{dadJoke}</p> ) } function App() { console.count('App') return ( <DadJokeProvider> <Button /> <DadJoke /> </DadJokeProvider> ) } export default App
Ici! Grâce à l'API que nous avons faite en utilisant les crochets. Nous ne modifierons plus ce fichier tout au long de la publication.
Commençons à ajouter des fonctionnalités à notre fichier de contexte, en commençant par l'état de
DadJokeProvider
. Oui, nous pourrions simplement utiliser le hook
useState
, mais gérons plutôt notre état via le
reducer
en ajoutant simplement la fonctionnalité
Redux
bien connue et bien-aimée.
function reducer(state, action) { switch (action.type) { case 'SET_DAD_JOKE': return { ...state, dadJoke: action.payload, } default: return new Error(); } }
Maintenant, nous pouvons passer ce réducteur au crochet
useReducer
et obtenir des blagues avec l'API:
export function DadJokeProvider({ children }) { const [state, dispatch] = React.useReducer(reducer, { dadJoke: null }) async function fetchDadJoke() { const response = await fetch('https://icanhazdadjoke.com', { headers: { accept: 'application/json', }, }) const data = await response.json() dispatch({ type: 'SET_DAD_JOKE', payload: data.joke, }) } const actions = { fetchDadJoke, } return ( <DadJokeContext.Provider value={{ state, actions }}> {children} </DadJokeContext.Provider> ) }
Devrait fonctionner! Cliquez sur le bouton devrait recevoir et afficher des blagues!
Vérifions la console:
App: 1 Button: 1 DadJoke: 1 Button: 2 DadJoke: 2 Button: 3 DadJoke: 3
Les deux composants sont rendus à chaque fois que l'état est mis à jour, mais un seul d'entre eux l'utilise réellement. Imaginez une vraie application dans laquelle des centaines de composants n'utilisent que des actions. Serait-il bien que nous puissions fournir tous ces rendus facultatifs?
Et ici, nous entrons sur le territoire de l'égalité relative, donc un petit rappel:
const obj = {}
Un composant qui utilise le contexte sera rendu à chaque fois que la valeur de ce contexte change. Voyons la signification de notre fournisseur de contexte:
<DadJokeContext.Provider value={{ state, actions }}>
Ici, nous créons un nouvel objet pendant chaque rendu, mais cela est inévitable, car un nouvel objet sera créé chaque fois que nous effectuons une action (
dispatch
), il est donc tout simplement impossible de mettre en cache (
memoize
) cette valeur.
Et tout cela ressemble à la fin de l'histoire, non?
Si nous regardons la fonction
fetchDadJoke
, la seule chose qu'elle utilise dans la portée externe est la
dispatch
, non? En général, je vais vous dire un petit secret sur les fonctions créées dans
useReducer
et
useState
. Par souci de concision, j'utiliserai
useState
comme exemple:
let prevSetCount function Counter() { const [count, setCount] = React.useState() if (typeof prevSetCount !== 'undefined') { console.log(setCount === prevSetCount) } prevSetCount = setCount return ( <button type="button" onClick={() => { setCount(count => count + 1) }}> Increment </button> ) }
Cliquez plusieurs fois sur le bouton et regardez la console:
true true true
Vous remarquerez que
setCount
la même fonction pour chaque rendu. Cela vaut également pour notre fonction d'
dispatch
.
Cela signifie que notre fonction
fetchDadJoke
dépend de rien qui change au fil du temps et ne dépend d'aucun autre générateur d'action, donc l'objet action ne doit être créé qu'une seule fois, lors du premier rendu:
const actions = React.useMemo(() => ({ fetchDadJoke, }), [])
Maintenant que nous avons un objet mis en cache avec des actions, pouvons-nous optimiser la valeur du contexte? En fait, non, car peu importe l'optimisation de l'objet de valeur, nous devons toujours en créer un nouveau à chaque fois en raison des changements d'état. Cependant, que se passe-t-il si nous déplaçons un objet d'action d'un contexte existant vers un nouveau? Qui a dit que nous ne pouvons avoir qu'un seul contexte?
const DadJokeStateContext = React.createContext() const DadJokeActionsContext = React.createContext()
Nous pouvons combiner les deux contextes dans notre
DadJokeProvider
:
return ( <DadJokeStateContext.Provider value={state}> <DadJokeActionsContext.Provider value={actions}> {children} </DadJokeActionsContext.Provider> </DadJokeStateContext.Provider> )
Et peaufiner nos crochets:
export function useDadJokeState() { return React.useContext(DadJokeStateContext) } export function useDadJokeActions() { return React.useContext(DadJokeActionsContext) }
Et nous avons terminé! Sérieusement, téléchargez autant de blagues que vous le souhaitez et voyez par vous-même.
App: 1 Button: 1 DadJoke: 1 DadJoke: 2 DadJoke: 3 DadJoke: 4 DadJoke: 5
Vous avez donc implémenté votre propre solution de gestion d'état optimisée! Vous pouvez créer différents fournisseurs en utilisant ce modèle à deux contextes pour créer votre application, mais ce n'est pas tout, vous pouvez également rendre le même composant fournisseur plusieurs fois! Quoi?! Oui, essayez le rendu
DadJokeProvider
à plusieurs endroits et voyez comment votre implémentation de gestion d'état évolue facilement!
Laissez libre cours à votre imagination et découvrez pourquoi vous avez vraiment besoin de
Redux
.
Merci à Kent C. Dodds pour les articles sur le modèle à deux contextes. Je ne l'ai vu nulle part ailleurs et il me semble que cela change les règles du jeu.
Lisez les articles de blog Kent suivants pour plus d'informations sur les concepts dont j'ai parlé:
Quand utiliser useMemo et useCallbackComment optimiser la valeur du contexteComment utiliser React Context efficacementGestion de l'état de l'application dans React.Une astuce simple pour optimiser les re-rendus dans React