Redux - Pas nécessaire! Remplacer par useContext et useReducer dans React?

image


Bonjour, Khabrovsk!


Je veux parler de la façon dont j'ai récemment appris l'existence de certains «crochets» dans React. Ils sont apparus relativement récemment, dans la version [16.8.0] du 6 février 2019 (qui, selon les vitesses de développement de FrontEnd, est déjà très ancienne)


Après avoir lu la documentation, je me suis concentré sur le hook useReducer et me suis immédiatement posé la question: "Cette chose peut remplacer complètement Redux!?" J'ai passé plusieurs soirées sur des expériences et maintenant je veux partager les résultats et mes conclusions.


Dois-je remplacer Redux par useContext + useReducer?


Pour les impatients - conclusions immédiates


Pour:


  • Vous pouvez utiliser des crochets (useContext + useReducer) à la place de Redux dans les petites applications (où il n'est pas nécessaire d'utiliser de grands réducteurs combinés). Dans ce cas, Redux peut en effet être redondant.

Contre:


  • Une grande quantité de code a déjà été écrite sur un tas de React + Redux et le réécrire pour les hooks (useContext + useReducer) me semble inapproprié, du moins pour l'instant.
  • Redux est une bibliothèque éprouvée, les hooks sont une innovation, leurs interfaces et leur comportement pourraient changer à l'avenir.
  • Afin de rendre l'utilisation de useContext + useReducer très pratique, vous devrez écrire des vélos.

Les conclusions sont l'opinion personnelle de l'auteur et ne prétendent pas être une vérité inconditionnelle - si vous n'êtes pas d'accord, je serai heureux de voir votre critique constructive dans les commentaires.


Essayons de comprendre


Commençons par un exemple simple.


(reducer.js)


import React from "react"; export const ContextApp = React.createContext(); export const initialState = { app: { test: 'test_context' } }; export const testReducer = (state, action) => { switch(action.type) { case 'test_update': return { ...state, ...action.payload }; default: return state } }; 

Jusqu'à présent, notre réducteur est exactement le même que dans Redux


(app.js)


 import React, {useReducer} from 'react' import {ContextApp, initialState, testReducer} from "./reducer.js"; import {IndexComponent} from "./IndexComponent.js" export const App = () => { //  reducer   state + dispatch   const [state, dispatch] = useReducer(testReducer, initialState); return ( //  ,     reducer   //  ContextApp   (dispatch  state) //      <ContextApp.Provider value={{dispatch, state}}> <IndexComponent/> </ContextApp.Provider> ) }; 

(IndexComponent.js)


 import React, {useContext} from "react"; import {ContextApp} from "./reducer.js"; export function IndexComponent() { //   useContext    ContextApp //  IndexComponent      ContextApp.Provider const {state, dispatch} = useContext(ContextApp); return ( //  dispatch    reducer.js   testReducer //    .    Redux <div onClick={() => {dispatch({ type: 'test_update', payload: { newVar: 123 } })}}> {JSON.stringify(state)} </div> ) } 

Ceci est l'exemple le plus simple dans lequel nous avons simplement mettre à jour écrire de nouvelles données dans un réducteur plat (sans imbrication)
En théorie, vous pouvez même essayer d'écrire comme ceci:


(reducer.js)


 ... export const testReducer = (state, data) => { return { ...state, ...data } ... 

(IndexComponent.js)


 ... return ( //      ,   type <div onClick={() => {dispatch({ newVar: 123 }> {JSON.stringify(state)} </div> ) ... 

Si nous n'avons pas d'application volumineuse et simple (ce qui est rarement le cas en réalité), alors vous ne pouvez pas utiliser de type et toujours gérer les mises à jour du réducteur directement depuis l'action. Soit dit en passant, au détriment des mises à jour, dans ce cas, nous n'avons écrit que de nouvelles données dans le réducteur, mais que faire si nous devons changer une valeur dans un arbre avec plusieurs niveaux d'imbrication?


Plus compliqué maintenant


Regardons l'exemple suivant:


(IndexComponent.js)


 ... return ( //        //     -     //      ,     callback: <div onClick={() => { //  ,    callback, //   testReducer     state (state) => { const {tree_1} = state; return { tree_1: { ...tree_1, tree_2_1: { ...tree_1.tree_2_1, tree_3_1: 'tree_3_1 UPDATE' }, }, }; }> {JSON.stringify(state)} </div> ) ... 

(reducer.js)


 ... export const initialState = { tree_1: { tree_2_1: { tree_3_1: 'tree_3_1', tree_3_2: 'tree_3_2' }, tree_2_2: { tree_3_3: 'tree_3_3', tree_3_4: 'tree_3_4' } } }; export const testReducer = (state, callback) => { //      state      //      callback const action = callback(state); return { ...state, ...action } ... 

D'accord, nous avons également compris la mise à jour de l'arborescence. Bien que dans ce cas, il est déjà préférable de revenir à l'utilisation de types dans testReducer et de mettre à jour l'arborescence en fonction d'un certain type d'action. Tout est comme dans Redux, seul le bundle résultant est légèrement plus petit [8].


Opérations asynchrones et répartition


Mais tout va bien? Que se passe-t-il si nous utilisons des opérations asynchrones?
Pour ce faire, nous devrons définir notre propre expédition. Essayons!


(action.js)


 export const actions = { sendToServer: function ({dataForServer}) { //      ,   dispatch return function (dispatch) { //   dispatch    , //   state      dispatch(state => { return { pending: true } }); } } 

(IndexComponent.js)


 const [state, _dispatch] = useReducer(AppReducer, AppInitialState); //     dispatch   -> //    ,  Proxy const dispatch = (action) => action(_dispatch); ... dispatch(actions.sendToServer({dataForServer: 'data'})) ... 

Tout semble aller bien aussi, mais maintenant nous avons beaucoup d'imbrication de rappel , ce qui n'est pas très cool, si nous voulons juste changer l'état sans créer une fonction d'action, nous devrons écrire une construction de ce type:


(IndexComponent.js)


 ... dispatch( (dispatch) => dispatch(state => { return { {dataForServer: 'data'} } }) ) ... 

Il s'avère que quelque chose d'effrayant, non? Pour une simple mise à jour des données, j'aimerais beaucoup écrire quelque chose comme ceci:


(IndexComponent.js)


 ... dispatch({dataForServer: 'data'}) ... 

Pour ce faire, vous devrez modifier le proxy de la fonction de répartition que nous avons créée précédemment
(IndexComponent.js)


 const [state, _dispatch] = useReducer(AppReducer, AppInitialState); //  // const dispatch = (action) => action(_dispatch); //  const dispatch = (action) => { if (typeof action === "function") { action(_dispatch); } else { _dispatch(() => action) } }; ... 

Maintenant, nous pouvons passer à la fois une fonction d'action et un simple objet à envoyer.
Mais! Avec un simple transfert de l'objet, il faut être prudent, vous pourriez être tenté de le faire:


(IndexComponent.js)


 ... dispatch({ tree: { //  state         AppContext ...state.tree, data: 'newData' } }) ... 

Pourquoi cet exemple est-il mauvais? Du fait qu'au moment où cette répartition a été traitée, l'état aurait pu être mis à jour via une autre répartition, mais ces modifications n'ont pas encore atteint notre composant, et en fait, nous utilisons une ancienne instance d'état qui écrasera tout avec les anciennes données.


Pour cette raison, une telle méthode ne convient pratiquement nulle part, uniquement pour la mise à jour des réducteurs plats dans lesquels il n'y a pas d'imbrication et vous n'avez pas besoin d'utiliser l'état pour mettre à jour les objets imbriqués. En réalité, les réducteurs sont rarement parfaitement plats, je vous conseille donc de ne pas utiliser cette méthode du tout et de ne mettre à jour les données que par des actions.


(action.js)


 ... // ..  dispatch   callback,    //       (. reducer.js) dispatch(state => { return { dataFromServer: { ...state.dataFromServer, form_isPending: true } } }); axios({ method: 'post', url: `...`, data: {...} }).then(response => { dispatch(state => { //   axios     //         dispatch //     ,  state -    , // ..       testReducer (reducer.js) return { dataFromServer: { ...state.dataFromServer, form_isPending: false, form_request: response.data }, user: {} } }); }).catch(error => { dispatch(state => { // , state -    ) return { dataFromServer: { ...state.dataFromServer, form_isPending: false, form_request: { error: error.response.data } }, } }); ... 

Conclusions:


  • Ce fut une expérience intéressante, j'ai renforcé mes connaissances académiques et appris de nouvelles fonctionnalités de la réaction
  • Je n'utiliserai pas cette approche en production (au moins dans les six prochains mois). Pour les raisons déjà décrites ci-dessus (il s'agit d'une nouvelle fonctionnalité et Redux est un outil éprouvé et fiable) + je n'ai aucun problème de performances à courir après les millisecondes que vous pouvez gagner en abandonnant l'éditeur [8]

Je serai heureux de connaître, dans les commentaires, l'avis des collègues de la partie front-end de notre Habrosobschestva!


Références:


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


All Articles