API de contexte Redux vs React



Dans React 16.3, une nouvelle API de contexte a été ajoutée. Nouveau dans le sens où l' ancienne API de contexte était dans les coulisses, la plupart des gens ne connaissaient pas son existence ou ne l'utilisaient pas, car la documentation conseillait d'éviter de l'utiliser.

Cependant, l'API Context est désormais une partie à part entière de React, ouverte à l'utilisation (pas comme auparavant, officiellement).

Immédiatement après la sortie de React 16.3, des articles sont apparus annonçant la mort de Redux en raison de la nouvelle API Context. Si vous aviez demandé à Redux à ce sujet, je pense qu'il aurait répondu - "les rapports de ma mort sont grandement exagérés ."

Dans cet article, je veux parler du fonctionnement de la nouvelle API Context, de son apparence à Redux, du moment où vous pouvez utiliser Context au lieu de Redux et pourquoi Context ne remplace pas Redux dans chaque cas.

Si vous voulez juste un aperçu de l'API Context, vous pouvez suivre le lien .

Exemple d'application React


Je vais suggérer que vous comprenez les principes de travail avec l'état dans React (accessoires et état), mais si ce n'est pas le cas, j'ai un cours gratuit de 5 jours pour vous aider à en savoir plus .

Regardons un exemple qui nous amène au concept utilisé dans Redux. Nous allons commencer avec une version simple de React, puis voir à quoi cela ressemble dans Redux et enfin avec Context.



Dans cette application, les informations utilisateur sont affichées à deux endroits: dans la barre de navigation dans le coin supérieur droit et dans le panneau latéral à côté du contenu principal.

(Vous remarquerez peut-être qu'il y a beaucoup de similitudes avec Twitter. Ce n'est pas une coïncidence! L'une des meilleures façons de perfectionner vos compétences React est de copier (créer des répliques de sites / applications existants) .

La structure du composant ressemble à ceci:



En utilisant Pure React (uniquement des accessoires), nous devons stocker les informations utilisateur suffisamment haut dans l'arborescence pour qu'elles puissent être transmises aux composants qui en ont besoin. Dans ce cas, les informations utilisateur doivent se trouver dans l'application.

Ensuite, afin de transférer des informations sur l'utilisateur vers les composants qui en ont besoin, l'application doit les transmettre à Nav and Body. Ils, à leur tour, le passeront à UserAvatar (hourra!) Et à la barre latérale. Enfin, Sidebar devrait le transmettre à UserStats.

Voyons comment cela fonctionne dans le code (je mets tout dans un seul fichier pour le rendre plus facile à lire, mais en fait il sera probablement divisé en fichiers séparés, suivant une structure standard ).

import React from "react"; import ReactDOM from "react-dom"; import "./styles.css"; const UserAvatar = ({ user, size }) => ( <img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> ); const UserStats = ({ user }) => ( <div className="user-stats"> <div> <UserAvatar user={user} /> {user.name} </div> <div className="stats"> <div>{user.followers} Followers</div> <div>Following {user.following}</div> </div> </div> ); const Nav = ({ user }) => ( <div className="nav"> <UserAvatar user={user} size="small" /> </div> ); const Content = () => <div className="content">main content here</div>; const Sidebar = ({ user }) => ( <div className="sidebar"> <UserStats user={user} /> </div> ); const Body = ({ user }) => ( <div className="body"> <Sidebar user={user} /> <Content user={user} /> </div> ); class App extends React.Component { state = { user: { avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b", name: "Dave", followers: 1234, following: 123 } }; render() { const { user } = this.state; return ( <div className="app"> <Nav user={user} /> <Body user={user} /> </div> ); } } ReactDOM.render(<App />, document.querySelector("#root")); 


Exemple de code CodeSandbox

Ici, l'application initialise l'état contenant l'objet utilisateur. Dans une application réelle, vous êtes le plus susceptible d' extraire ces données du serveur et de les enregistrer dans un état pour le rendu.

En ce qui concerne les accessoires ("forage de prop"), ce n'est pas un gros problème . Cela fonctionne très bien. Jeter des accessoires, c'est un exemple idéal de React. Mais jeter profondément dans l'arbre d'état peut être un peu ennuyeux lors de l'écriture. Et d'autant plus ennuyeux si vous devez transmettre beaucoup d'accessoires (et pas un).

Cependant, il y a un gros inconvénient dans cette stratégie: elle crée une connexion entre les composants qui ne devraient pas être connectés. Dans l'exemple ci-dessus, Nav doit accepter un accessoire «utilisateur» et le transmettre à UserAvatar, même si Nav n'en a pas besoin.

Les composants étroitement couplés (tels que ceux qui transmettent des accessoires à leurs enfants) sont plus difficiles à réutiliser, car vous devez les lier à de nouveaux parents chaque fois que vous les utilisez dans un nouvel endroit.

Voyons comment nous pouvons améliorer cela.

Avant d'utiliser Context ou Redux ...


Si vous pouvez trouver un moyen de combiner la structure de votre application et de tirer parti des accessoires aux descendants, cela peut rendre votre code plus propre sans avoir à recourir à un avant-goût profond des accessoires, du contexte ou de Redux .

Dans cet exemple, les accessoires pour enfants sont une excellente solution pour les composants qui doivent être universels, tels que Nav, Sidebar et Body. Sachez également que vous pouvez transmettre JSX à n'importe quel accessoire, pas seulement aux enfants. Par conséquent, si vous avez besoin de plus d'un «emplacement» pour connecter des composants, n'oubliez pas ceci.

Voici un exemple d'application React dans laquelle Nav, Body et Sidebar prennent des enfants et les affichent tels quels. Ainsi, celui qui utilise le composant n'a pas à se soucier du transfert de certaines données dont le composant a besoin. Il peut simplement afficher ce dont il a besoin sur place, en utilisant les données dont il dispose déjà. Cet exemple montre également comment utiliser n'importe quel accessoire pour transmettre des enfants.

(Merci à Dan Abramov pour cette offre !)

 import React from "react"; import ReactDOM from "react-dom"; import "./styles.css"; const UserAvatar = ({ user, size }) => ( <img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> ); const UserStats = ({ user }) => ( <div className="user-stats"> <div> <UserAvatar user={user} /> {user.name} </div> <div className="stats"> <div>{user.followers} Followers</div> <div>Following {user.following}</div> </div> </div> ); //  children   . const Nav = ({ children }) => ( <div className="nav"> {children} </div> ); const Content = () => ( <div className="content">main content here</div> ); const Sidebar = ({ children }) => ( <div className="sidebar"> {children} </div> ); // Body   sidebar  content,    , //    . const Body = ({ sidebar, content }) => ( <div className="body"> <Sidebar>{sidebar}</Sidebar> {content} </div> ); class App extends React.Component { state = { user: { avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b", name: "Dave", followers: 1234, following: 123 } }; render() { const { user } = this.state; return ( <div className="app"> <Nav> <UserAvatar user={user} size="small" /> </Nav> <Body sidebar={<UserStats user={user} />} content={<Content />} /> </div> ); } } ReactDOM.render(<App />, document.querySelector("#root")); 


Exemple de code CodeSandbox

Si votre application est trop compliquée (plus compliquée que cet exemple!), Il peut être difficile de comprendre comment adapter le modèle aux enfants. Voyons comment vous pouvez remplacer le transfert d'accessoires par Redux.

Exemple de redux


Je vais jeter un coup d'œil à l'exemple de Redux afin que nous puissions avoir une compréhension plus approfondie du fonctionnement du contexte, donc si vous n'avez pas une compréhension claire de Redux, lisez d'abord mon introduction à Redux (ou regardez la vidéo ).

Voici notre application React repensée pour utiliser Redux. Les informations utilisateur ont été déplacées vers la boutique Redux, ce qui signifie que nous pouvons utiliser la fonction de connexion react-redux pour transmettre directement l'hélice utilisateur aux composants qui en ont besoin.

Il s'agit d'une grande victoire en termes de suppression de la connectivité. Jetez un œil à Nav, Body et Sidebar, et vous verrez qu'ils ne reçoivent ni ne transmettent plus d'accessoires utilisateur. Ils ne jouent plus de patates chaudes avec des accessoires. Plus de connexions inutiles.

Le réducteur fait peu ici; c'est assez simple. J'ai une dernière chose sur le fonctionnement des réducteurs Redux et sur la façon d'écrire le code immuable qu'ils utilisent.

 import React from "react"; import ReactDOM from "react-dom"; //    createStore, connect, and Provider: import { createStore } from "redux"; import { connect, Provider } from "react-redux"; //  reducer       . const initialState = {}; function reducer(state = initialState, action) { switch (action.type) { //    action SET_USER  state. case "SET_USER": return { ...state, user: action.user }; default: return state; } } //  store  reducer'   . const store = createStore(reducer); // Dispatch' action     user. store.dispatch({ type: "SET_USER", user: { avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b", name: "Dave", followers: 1234, following: 123 } }); //   mapStateToProps,      state (user) //     `user` prop. const mapStateToProps = state => ({ user: state.user }); //  UserAvatar    connect(),    //`user` ,      . //     2 : // const UserAvatarAtom = ({ user, size }) => ( ... ) // const UserAvatar = connect(mapStateToProps)(UserAvatarAtom); const UserAvatar = connect(mapStateToProps)(({ user, size }) => ( <img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> )); //   UserStats    connect(),    // `user` . const UserStats = connect(mapStateToProps)(({ user }) => ( <div className="user-stats"> <div> <UserAvatar /> {user.name} </div> <div className="stats"> <div>{user.followers} Followers</div> <div>Following {user.following}</div> </div> </div> )); //    Nav      `user`. const Nav = () => ( <div className="nav"> <UserAvatar size="small" /> </div> ); const Content = () => ( <div className="content">main content here</div> ); //   Sidebar. const Sidebar = () => ( <div className="sidebar"> <UserStats /> </div> ); //   Body. const Body = () => ( <div className="body"> <Sidebar /> <Content /> </div> ); //  App    ,     . const App = () => ( <div className="app"> <Nav /> <Body /> </div> ); //     Provider, //   connect()    store. ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.querySelector("#root") ); 


Exemple de code CodeSandbox

Maintenant, vous vous demandez probablement comment Redux réalise cette magie. Incroyable Comment React ne prend-il pas en charge le passage d'accessoires à plusieurs niveaux, et Redux peut-il le faire?

La réponse est que Redux utilise la fonction de contexte React (fonction de contexte). Pas l'API Context moderne (pas encore), mais l'ancienne. Celui que la documentation React dit de ne pas utiliser, sauf si vous écrivez votre propre bibliothèque ou si vous savez ce que vous faites.

Le contexte est similaire à un bus d'ordinateur suivant chaque composant: pour obtenir la puissance (les données) qui le traverse, il suffit de se connecter. Et react-redux connect fait exactement cela.

Cependant, cette fonctionnalité Redux n'est que la pointe de l'iceberg. Le transfert de données au bon endroit est la caractéristique la plus évidente de Redux. Voici quelques autres avantages que vous obtenez hors de la boîte:

se connecter est une pure fonction

connect rend automatiquement les composants connectés «propres», c'est-à-dire qu'ils ne seront restitués que lorsque leurs accessoires changent, c'est-à-dire lorsque leur tranche de l'état Redux change. Cela évite un nouveau rendu inutile et accélère l'application.

Débogage facile avec Redux

La cérémonie d'écriture des actions et des réducteurs est équilibrée par l'incroyable facilité de débogage que Redux vous offre.

Avec l'extension Redux DevTools, vous obtenez un journal automatique de toutes les actions effectuées par votre application. À tout moment, vous pouvez l'ouvrir et voir quelles actions ont été lancées, quelle est leur charge utile et indiquer avant et après l'action.



Une autre fonctionnalité intéressante de Redux DevTools est le débogage en utilisant le "voyage dans le temps" , c'est-à-dire que vous pouvez cliquer sur n'importe quelle action précédente et aller à ce point dans le temps, jusqu'à l'actuelle. La raison pour laquelle cela fonctionne est que chaque action met à jour le magasin de la même manière , vous pouvez donc prendre une liste des mises à jour d'état enregistrées et les lire sans aucun effet secondaire, et vous retrouver à l'endroit souhaité.

Il existe également des outils comme LogRocket , qui vous donnent essentiellement les Redux DevTools permanents en production pour chacun de vos utilisateurs. Vous avez un rapport de bug? Pas de problème. Regardez cette session utilisateur dans LogRocket, et vous pouvez voir une répétition de ce qu'il a fait et des actions qui ont été lancées. Tout cela fonctionne en utilisant le flux d'actions Redux.

Extension de Redux avec un middleware

Redux prend en charge le concept de middleware (un mot de fantaisie pour «une fonction qui s'exécute chaque fois qu'une action est envoyée»). Écrire votre propre middleware n'est pas aussi difficile qu'il y paraît et vous permet d'utiliser des outils puissants.

Par exemple ...

  • Vous souhaitez envoyer une demande d'API chaque fois qu'un nom d'action commence par FETCH_? Vous pouvez le faire avec un middleware.
  • Vous voulez un emplacement centralisé pour enregistrer les événements dans votre logiciel d'analyse? Le middleware est un bon endroit pour le faire.
  • Vous voulez empêcher une action de démarrer à un moment précis? Vous pouvez le faire avec un middleware, invisible pour le reste de votre application.
  • Vous souhaitez intercepter une action dotée d'un jeton JWT et l'enregistrer automatiquement dans localStorage? Oui, middleware.

Voici un bon article avec des exemples d'écriture de middleware Redux.

Comment utiliser l'API de contexte React


Mais peut-être n'avez-vous pas besoin de toutes ces bizarreries Redux. Vous n'aurez peut-être pas besoin d'un simple débogage, d'un réglage ou d'une amélioration automatique des performances - tout ce que vous voulez faire est de transférer facilement des données. Peut-être que votre application est petite ou que vous avez juste besoin de faire quelque chose rapidement et de traiter les subtilités plus tard.

La nouvelle API de contexte vous convient probablement. Voyons comment cela fonctionne.

J'ai posté un rapide tutoriel sur l'API Context sur Egghead si vous préférez regarder plutôt que lire (3:43).

Voici 3 composants importants de l'API Context:

  • Fonction React.createContext qui crée le contexte
  • Provider (renvoie createContext), qui configure le "bus",
    en passant par l'arborescence des composants
  • Consommateur (également createContext retourné) qui s'imprègne dans
    "Bus électrique" pour l'extraction des données

Provider est très similaire à Provider dans React-Redux. Il prend une valeur qui peut être tout ce que vous voulez (ce pourrait même être un magasin Redux ... mais ce serait stupide). Il s'agit très probablement d'un objet contenant vos données et toutes les actions que vous souhaitez effectuer avec les données.

Le consommateur fonctionne un peu comme la fonction de connexion dans React-Redux, se connectant aux données et les mettant à la disposition d'un composant qui les utilise.

Voici les faits saillants:

 //     context //    2 : { Provider, Consumer } // ,   ,  UpperCase,  camelCase //  ,          , //        . const UserContext = React.createContext(); // ,     context, //   Consumer. // Consumer   "render props". const UserAvatar = ({ size }) => ( <UserContext.Consumer> {user => ( <img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> )} </UserContext.Consumer> ); // ,      "user prop", //   Consumer    context. const UserStats = () => ( <UserContext.Consumer> {user => ( <div className="user-stats"> <div> <UserAvatar user={user} /> {user.name} </div> <div className="stats"> <div>{user.followers} Followers</div> <div>Following {user.following}</div> </div> </div> )} </UserContext.Consumer> ); // ...    ... // ... (      `user`). //  App   context ,  Provider. class App extends React.Component { state = { user: { avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b", name: "Dave", followers: 1234, following: 123 } }; render() { return ( <div className="app"> <UserContext.Provider value={this.state.user}> <Nav /> <Body /> </UserContext.Provider> </div> ); } } 


Exemple de code CodeSandbox

Voyons comment cela fonctionne.

Rappelez-vous, nous avons 3 parties: le contexte lui-même (créé à l'aide de React.createContext) et deux composants qui interagissent avec lui (fournisseur et consommateur).

Le fournisseur et le consommateur travaillent ensemble

Le fournisseur et le consommateur sont liés et inséparables. Ils savent seulement comment interagir les uns avec les autres. Si vous avez créé deux contextes distincts, disons «Context1» et «Context2», le fournisseur et le consommateur Context1 ne pourront pas communiquer avec le fournisseur et le consommateur Context2.

Le contexte ne contient pas d'état

Notez que le contexte n'a pas son propre état . Ce n'est qu'un canal pour vos données. Vous devez transmettre la valeur au fournisseur, et cette valeur sera transmise à tout consommateur qui sait comment la rechercher (le fournisseur est lié au même contexte que le consommateur).

Lorsque vous créez un contexte, vous pouvez transmettre la "valeur par défaut" comme suit:

 const Ctx = React.createContext(yourDefaultValue); 


La valeur par défaut est ce que le consommateur recevra lorsqu'il sera placé dans l'arborescence sans le fournisseur au-dessus. Si vous ne le transmettez pas, la valeur ne sera pas définie. Notez qu'il s'agit de la valeur par défaut , pas de la valeur initiale . Le contexte ne sauvegarde rien; il diffuse simplement les données que vous lui transmettez.

Le consommateur utilise le modèle des accessoires de rendu

La fonction connect Redux est un composant d'ordre supérieur (abrégé en HoC). Il enveloppe un autre composant et y passe des accessoires.

Le consommateur, en revanche, s'attend à ce que le composant enfant soit une fonction. Ensuite, il appelle cette fonction pendant le rendu, en passant la valeur reçue du fournisseur quelque part au-dessus (soit la valeur par défaut du contexte, soit indéfinie si vous n'avez pas transmis la valeur par défaut).

Le fournisseur prend une valeur.

Juste une valeur, comme accessoire. Mais rappelez-vous que la valeur peut être n'importe quoi. En pratique, si vous souhaitez transmettre plusieurs valeurs, vous devez créer un objet avec toutes les valeurs et transmettre cet objet.

API de contexte flexible


Étant donné que la création de contexte nous donne deux composants avec lesquels travailler (fournisseur et consommateur), nous pouvons les utiliser comme nous le souhaitons. Voici quelques idées.

Envelopper le consommateur dans HOC

Vous n'aimez pas l'idée d'ajouter UserContext.Consumer autour de chaque endroit qui en a besoin? Ceci est votre code! Vous avez le droit de décider quel sera votre meilleur choix.

Si vous préférez obtenir la valeur en tant que prop, vous pouvez écrire un petit wrapper autour de Consumer comme suit:

 function withUser(Component) { return function ConnectedComponent(props) { return ( <UserContext.Consumer> {user => <Component {...props} user={user}/>} </UserContext.Consumer> ); } } 

Après cela, vous pouvez réécrire, par exemple, UserAvatar en utilisant la fonction withUser:

 const UserAvatar = withUser(({ size, user }) => ( <img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> )); 

Et le tour est joué, le contexte peut fonctionner comme connect Redux. Moins de propreté automatique.

Voici un exemple de CodeSandbox avec ce HOC.

Garder l'État dans le fournisseur

N'oubliez pas que le fournisseur n'est qu'un canal. Il n'enregistre aucune donnée. Mais cela ne vous empêche pas de créer votre propre wrapper pour stocker les données.

Dans l'exemple ci-dessus, les données sont stockées dans l'application, donc la seule chose que vous deviez comprendre était les composants Provider + Consumer. Mais vous souhaitez peut-être créer votre propre boutique. Vous pouvez créer un composant pour stocker l'état et les passer dans le contexte:

 class UserStore extends React.Component { state = { user: { avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b", name: "Dave", followers: 1234, following: 123 } }; render() { return ( <UserContext.Provider value={this.state.user}> {this.props.children} </UserContext.Provider> ); } } // ...    ... const App = () => ( <div className="app"> <Nav /> <Body /> </div> ); ReactDOM.render( <UserStore> <App /> </UserStore>, document.querySelector("#root") ); 

Désormais, les données utilisateur sont contenues dans son propre composant, dont la seule tâche est ces données. Cool. L'application peut redevenir apatride. Je pense que ça a l'air un peu plus propre.

Voici un exemple CodeSandbox avec ce UserStore.

Jetez les actions dans le contexte

N'oubliez pas que l'objet passé par le fournisseur peut contenir tout ce que vous voulez. Cela signifie qu'il peut contenir des fonctions. Vous pouvez même leur nommer des actions.

Voici un nouvel exemple: une pièce simple avec un interrupteur pour changer la couleur d'arrière-plan - oh, je veux dire la lumière.



L'état est stocké dans le magasin, qui a également une fonction de commutation de lumière. L'état et la fonction sont transmis à travers le contexte.

 import React from "react"; import ReactDOM from "react-dom"; import "./styles.css"; //  context. const RoomContext = React.createContext(); // ,     //   . class RoomStore extends React.Component { state = { isLit: false }; toggleLight = () => { this.setState(state => ({ isLit: !state.isLit })); }; render() { //  state  onToggleLight action return ( <RoomContext.Provider value={{ isLit: this.state.isLit, onToggleLight: this.toggleLight }} > {this.props.children} </RoomContext.Provider> ); } } //    ,    , //       RoomContext. const Room = () => ( <RoomContext.Consumer> {({ isLit, onToggleLight }) => ( <div className={`room ${isLit ? "lit" : "dark"}`}> The room is {isLit ? "lit" : "dark"}. <br /> <button onClick={onToggleLight}>Flip</button> </div> )} </RoomContext.Consumer> ); const App = () => ( <div className="app"> <Room /> </div> ); //     RoomStore, //           . ReactDOM.render( <RoomStore> <App /> </RoomStore>, document.querySelector("#root") ); 

Voici un exemple de travail complet dans CodeSandbox .

Alors, quoi utiliser, Context ou Redux?

Maintenant que vous avez vu les deux chemins, lequel vaut la peine d'être utilisé? Je sais que vous voulez juste entendre la réponse à cette question, mais je dois répondre - "cela dépend de vous".

Cela dépend de la taille de votre application ou de sa vitesse de croissance. Combien de personnes y travailleront - seulement vous ou une grande équipe? Quelle est votre expérience ou celle de votre équipe pour travailler avec les concepts fonctionnels sur lesquels Redux s'appuie (tels que l'immuabilité et les fonctionnalités pures).

Une erreur fatale imprégnant tout l'écosystème JavaScript est l'idée de la concurrence . Il y a une idée que chaque choix est un jeu à somme nulle: si vous utilisez la bibliothèque A, vous ne devriez pas utiliser sa bibliothèque concurrente B.Quand une nouvelle bibliothèque sort mieux que la précédente, elle devrait évincer la bibliothèque existante. Que tout doit être soit / ou que vous devez soit choisir le plus récent et le meilleur, soit être relégué au second plan avec les développeurs du passé.

La meilleure approche consiste à examiner ce merveilleux choix avec un exemple, un ensemble d'outils. C'est comme choisir entre utiliser un tournevis ou un tournevis puissant. Dans 80% des cas, un tournevis fera le travail plus facilement et plus rapidement qu'un tournevis. Mais pour les 20% restants, un tournevis serait le meilleur choix (pas assez d'espace, ou l'article est mince). Quand j'ai acheté un tournevis, je n'ai pas immédiatement jeté le tournevis, il ne l'a pas remplacé, mais m'a simplement donné une autre option. Une autre façon de résoudre le problème.

Le contexte ne «remplace» pas Redux, pas plus que React «remplace» Angular ou jQuery. Enfer, j'utilise toujours jQuery quand j'ai besoin de faire quelque chose rapidement. J'utilise encore parfois des modèles EJS côté serveur au lieu de déployer une application React. Parfois, React est plus que nécessaire pour effectuer une tâche. Il en va de même pour Redux.

Aujourd'hui, si Redux est plus que nécessaire, vous pouvez utiliser le contexte.

Learning React peut être difficile - il y a tellement de bibliothèques et d'outils!

Mes conseils Ignorez-les tous :)

Pour un didacticiel pas à pas, lisez mon livre , Pure React .

Source: https://habr.com/ru/post/fr419449/


All Articles