Redux - Not needed! Replace with useContext and useReducer in React?

image







Good day, Khabrovsk!







I want to talk about how I recently learned about certain “hooks” in React. They appeared relatively recently, in the version [16.8.0] of February 6, 2019 (which, according to the development speeds of FrontEnd, is already a very long time ago)







After reading the documentation, I focused on the useReducer hook and immediately asked myself the question: "This thing can completely replace Redux !?" I spent several evenings on experiments and now I want to share the results and my conclusions.







Do I need to replace Redux with useContext + useReducer?



For the impatient - immediately conclusions



Per:









Against:









The conclusions are the personal opinion of the author and do not claim to be unconditional truth - if you do not agree, I will be glad to see your constructive criticism in the comments.







Let's try to figure it out



Let's start with a simple example.







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





So far, our reducer looks exactly the same as in 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> ) }
      
      





This is the simplest example in which we simply update write new data to a flat (without nesting) reducer

In theory, you can even try to write like this:







(reducer.js)







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





(IndexComponent.js)







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





If we do not have a large and simple application (which is rarely the case in reality), then you can not use type and always manage the reducer update directly from the action. By the way, at the expense of updates, in this case we only wrote new data in reducer, but what if we have to change one value in a tree with several levels of nesting?







More complicated now



Let's look at the following example:







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





Okay, we figured out the tree update too. Although in this case it is already better to return to using types inside testReducer and update the tree according to a certain type of action. Everything is like in Redux, only the resulting bundle is slightly smaller [8].







Asynchronous operations and dispatch



But is everything all right? What happens if we go in to use asynchronous operations?

To do this, we will have to define our own dispatch. Let's try!







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





It seems that everything is okay too, but now we have a lot of nested callbacks , which is not very cool, if we just want to change the state without creating an action function, we will have to write a construction of this kind:







(IndexComponent.js)







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





It turns out something scary, right? For a simple update of the data, I would very much like to write something like this:







(IndexComponent.js)







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





To do this, you will have to change the Proxy for the dispatch function that we created earlier

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





Now we can pass both an action function and a simple object to dispatch.

But! With a simple transfer of the object, you must be careful, you may be tempted to do this:







(IndexComponent.js)







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





Why is this example bad? By the fact that by the time this dispatch was processed, state could have been updated through another dispatch, but these changes have not yet reached our component and in fact we are using an old state instance that will overwrite everything with old data.







For this reason, such a method becomes hardly suitable anywhere, only for updating flat reducers in which there is no nesting and you do not need to use state to update nested objects. In reality, reducers are rarely perfectly flat, so I would advise you not to use this method at all and only update data through 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:





I will be glad to know, in the comments, the opinion of colleagues from the front-end part of our Habrosobschestva!







References:






All Articles