Redux - ¡No es necesario! ¿Reemplazar con useContext y useReducer en React?

imagen


Buen día, Khabrovsk!


Quiero hablar sobre cómo recientemente aprendí sobre ciertos "ganchos" en React. Aparecieron relativamente recientemente, en la versión [16.8.0] del 6 de febrero de 2019 (que, según las velocidades de desarrollo de FrontEnd, ha sido hace mucho tiempo)


Después de leer la documentación, me concentré en el gancho useReducer e inmediatamente me hice la pregunta: "¿Esto puede reemplazar completamente a Redux?" Pasé varias tardes en experimentos y ahora quiero compartir los resultados y mis conclusiones.


¿Necesito reemplazar Redux con useContext + useReducer?


Para los impacientes: conclusiones inmediatas


Para:


  • Puede usar ganchos (useContext + useReducer) en lugar de Redux en aplicaciones pequeñas (donde no hay necesidad de grandes reductores combinados). En este caso, Redux puede ser redundante.

En contra:


  • Ya se ha escrito una gran cantidad de código en un grupo de React + Redux, y reescribirlo para ganchos (useContext + useReducer) me parece inapropiado, al menos por ahora.
  • Redux es una biblioteca probada, los ganchos son una innovación, sus interfaces y comportamiento pueden cambiar en el futuro.
  • Para que el uso de useContext + useReducer sea realmente conveniente, tendrá que escribir algunas bicicletas.

Las conclusiones son la opinión personal del autor y no pretenden ser una verdad incondicional; si no está de acuerdo, me complacerá ver su crítica constructiva en los comentarios.


Tratemos de resolverlo


Comencemos con un ejemplo 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 } }; 

Hasta ahora, nuestro reductor se ve exactamente igual que en 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> ) } 

Este es el ejemplo más simple en el que simplemente actualizar escribir datos nuevos en un reductor plano (sin anidamiento)
En teoría, incluso puedes intentar escribir así:


(reducer.js)


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

(IndexComponent.js)


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

Si no tenemos una aplicación grande y simple (que rara vez es el caso en realidad), entonces no puede usar el tipo y siempre administrar la actualización del reductor directamente desde la acción. Por cierto, a expensas de las actualizaciones, en este caso solo escribimos nuevos datos en reductor, pero ¿qué pasa si tenemos que cambiar un valor en un árbol con varios niveles de anidamiento?


Más complicado ahora


Veamos el siguiente ejemplo:


(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 } ... 

De acuerdo, también descubrimos la actualización del árbol. Aunque en este caso ya es mejor volver a usar los tipos dentro de testReducer y actualizar el árbol de acuerdo con cierto tipo de acción. Todo es como en Redux, solo el paquete resultante es ligeramente más pequeño [8].


Operaciones asincrónicas y despacho


¿Pero está todo bien? ¿Qué sucede si vamos a usar operaciones asincrónicas?
Para hacer esto, tenemos que definir nuestro propio despacho. ¡Intentémoslo!


(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'})) ... 

Todo parece estar bien también, pero ahora tenemos un montón de anidamiento de devolución de llamada , lo que no es muy bueno, si solo queremos cambiar el estado sin crear una función de acción, tendremos que escribir una construcción de este tipo:


(IndexComponent.js)


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

Resulta algo aterrador, ¿verdad? Para una simple actualización de datos, realmente me gustaría escribir algo como esto:


(IndexComponent.js)


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

Para hacer esto, debe cambiar el Proxy para la función de envío que creamos anteriormente
(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) } }; ... 

Ahora podemos pasar tanto una función de acción como un objeto simple para despachar.
Pero! Con una simple transferencia del objeto, debe tener cuidado, puede sentirse tentado a hacer esto:


(IndexComponent.js)


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

¿Por qué es malo este ejemplo? Por el hecho de que en el momento en que se procesó este envío, el estado podría haberse actualizado a través de otro envío, pero estos cambios aún no han llegado a nuestro componente, y de hecho estamos utilizando una instancia de estado anterior que sobrescribirá todo con datos antiguos.


Por esta razón, dicho método apenas vale la pena, solo para actualizar reductores planos en los que no hay anidamiento y no es necesario contactar el estado para actualizar los objetos anidados. En realidad, los reductores rara vez son perfectamente planos, por lo que le aconsejaría que no utilice este método y que solo actualice los datos a través de acciones.


(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 } }, } }); ... 

Conclusiones:


  • Fue una experiencia interesante, fortalecí mi conocimiento académico y aprendí nuevas características de la reacción.
  • No utilizaré este enfoque en la producción (al menos en los próximos seis meses). Por las razones ya descritas anteriormente (esta es una característica nueva, y Redux es una herramienta probada y confiable) + No tengo problemas de rendimiento que perseguir después de los milisegundos que puedes ganar al abandonar el editor [8]

¡Me alegrará saber, en los comentarios, la opinión de colegas de la parte delantera de nuestra Habrosobschestva!


Referencias


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


All Articles