Let’s talk about Reducer Context and Redux again

Preface

I have been a Reactdeveloper for a long time, and I have developed many large and small applications. Except for Reactusing it when I was learning at the beginning Redux, I basically stopped touching it later. No matter how complex the application is, I simply feel that it is better to use it. ContextIt can solve all my problems. I am ashamed to say that I have basically never thought about Reduxthe reason for its existence. Maybe it is Reactreally done too well, or maybe our current equipment performance is seriously excessive, so I don’t need to consider application optimization at all.

The question of why so many people use it came up again today Redux, so I took another look at the documentation of Reactand Redux, and found out quite a lot (every time I read the documentation I gained something new, I recommend you to read it more if you have nothing to do), and suddenly it reminded me of There have been many times useStatewhere I felt awkward when updating an array (although there was nothing wrong with it, but I always felt it was too complicated). Let’s talk about this today.

Feel free to recommend using the examples/reducer-context-redux I wrote for better results. All the complete codes below can be found in the examples.

need

Talking about some technical things divorced from real needs always makes people feel empty, so today we will talk about a relatively simple requirement and compare the differences in code evolved in several different ways to help us understand RedcuerContextand Reduxthese concepts.

Briefly describe the requirements, an todoinput box can be created, a displayed todolist, todothe name itself can be modified, marked as completed or deleted, see the figure below for details.

General implementation

You can also see from the picture that this is a very simple requirement. Let us quickly implement it. For the complete code, see examples/reducer-conetxt-redux/base

...
 // Define a Todos 
const [todos, setTodos] = useState ([]);

// Define several methods to create, update, and delete Todos 
const  handleAddTodo = ( name ) => {
   setTodos ([...todos, { id : nextId++, name, done : false }]);
};

const  handleChangeTodo = ( todo ) => {
   setTodos (
    todos. map ( ( t ) => {
       if (t. id === todo. id ) {
         return todo;
      } else {
         return t;
      }
    })
  );
};

const  handleDelTodo = ( id ) => {
   setTodos (todos. filter ( ( t ) => t. id !== id));
};

Why did I only post part of the code? Because this part of the code will undergo the first step of evolution, I believe most people will write it this way (if not, don’t blame me, at least this is how I write it in most cases).

But what’s wrong with it? In fact, there is no problem. If you haven’t encountered any problems yet, there is indeed no problem. If you feel like you are talking nonsense, let me give you a few situations where you may encounter problems:

  1. If Todo needs to be created, updated and deleted through the interface, then when you perform multiple operations at the same time, todosonly the last update will be completed.
  2. There 1is a very uncomfortable part, that is, it is not easy to find out todoswhy it is not updated correctly to what you expect. This troubleshooting is very painful. I wonder if you have encountered it?
  3. useStateAlthough the update mechanism seems to be displayed very intuitively now todos, if I add more functions, such as multi-state todo, you will need more setTodosupdates at this time todos, which seems to be difficult to understand, then we can 更新change this operation to 上一步Or 下一步, do you need to unpack handleChangeTodothis method?

Okay, that’s almost the first version, so how to optimize it?

Reducer

To use it, you have to know what it is first, right? To put it simply, it is to merge all the status update logic into one function, which is called Reducer. Its definition has come out. I think at this time you may have thought of how Reducerto update the first version above. For the complete code, see examples/reducer-context-redux/reducer .

function  todoReducer ( todos, action ) {
   switch (action. type ) {
     case  'added' : {
       return [...todos, { id : nextId++, name : action. name , done : false }];
    }
    case  'changed' :
       return todos. map ( ( t ) => {
         if (t. id === action. todo . id ) {
           return action. todo ;
        } else {
           return t;
        }
      });
    case  'deleted' : {
       return todos. filter ( ( t ) => t. id !== action. id );
    }
    default : {
       throw  Error ( 'Unknown action: ' + action. type );
    }
  }
}

const [todos, dispatch] = useReducer (todoReducer, []);

const  handleAddTodo = ( name ) => {
   dispatch ({
     type : 'added' ,
    name,
  });
};

const  handleChangeTodo = ( todo ) => {
   dispatch ({
     type : 'changed' ,
    todo,
  });
};

const  handleDelTodo = ( id ) => {
   dispatch ({
     type : 'deleted' ,
    ID,
  });
};

Looking at it directly, it seems that there is more code than above? This example is indeed true, but just like what I said in the first version 问题3, when your status becomes more and more complex, the code growth brought about by the two methods will not be the same. In other words, when the number of statuses reaches a certain level, , the code written this way will be less, but this does not seem to be a sufficient reason to write this way.

Then let me add some benefits of writing like this:

  1. All status changes are contained in the function, and you can easily perceive the status changes todoReducerin this function . console.log-> Convenient for debugging
  2. todoReducerAs a clean function, you can easily write its test cases. -> Conveniently write test cases and enhance stability
  3. Status changes are clear at a glance. -> Enhance readability

Of course, these are not necessary. You can use them according to your preferences and scenarios , useStatebut useReduceryou should know the difference.

Context

The above code does Contextn’t seem to have any direct connection with it, so why should I take a look at it too? Because Reduxit is like a combination of Reducerand Context, so you now know Reducerwhat it is, and of course you also need to know Contextwhat it is. As mentioned above, see examples/reducer-conetxt-redux/context for
the complete code .

So Contextwhat is it? To put it simply, it means that two components that are not directly connected share state. For example A -> B -> C -> D, the status Dyou want to receive needs to go through the sum , so if you use it , you can skip the sum . Doesn’t it seem very useful? Indeed, it is, but it has a big shortcoming, which is why we don’t want it to be abused, because you need to define this state, then if this state changes, all sub-components will be updated, then if you are in a If a very large application has a frequently changing state defined at its root, then the application must be updated frequently, which is a scary thing.ABCContextBCAA

The main purpose of looking at it again Contextis to share state across components. It does not have the function of state definition and management, that is, it needs to be matched useStateor useReducerused. This is why I said Reduxit is like a combination of Reducerand Context. Let’s take a look at the code evolution. .

// TodoContext.jsx 
import { createContext } from  'react' ;

export  function  todoReducer ( todos, action ) {
   switch (action. type ) {
     case  'added' : {
       return [...todos, { id : nextId++, name : action. name , done : false }];
    }
    case  'changed' :
       return todos. map ( ( t ) => {
         if (t. id === action. todo . id ) {
           return action. todo ;
        } else {
           return t;
        }
      });
    case  'deleted' : {
       return todos. filter ( ( t ) => t. id !== action. id );
    }
    default : {
       throw  Error ( 'Unknown action: ' + action. type );
    }
  }
}

export  default  createContext ();

// 
import  TodoContext , { todoReducer } from  './context' ;

const [todos, dispatch] = useReducer (todoReducer, []);

return (
   < TodoContext.Provider  value = {{  todos , dispatch }}> 
    < ContextApp /> 
  </ TodoContext.Provider > 
);

Of course, you can still useStateuse it to replace it useReducer, it depends on how you think about it!

Redux

Finally we have reached the last step. In fact, if you don’t use it Redux, it doesn’t seem to prevent you from doing this requirement or any other requirement. But why should you use it? This is what we will explore today. For the complete code, see examples/reducer- conetxt-redux/redux
.

Let’s look at the code transformation first. Due to the new Reduxuse Toolkitto organize the code, but for the convenience of understanding, I will simply use it reduxto demonstrate this transformation.

// store.jsx 
import { createStore } from  'redux' ;

function  todoReducer ( todos = [], action ) {
   switch (action. type ) {
     case  'added' : {
       return [...todos, { id : nextId++, name : action. name , done : false }];
    }
    case  'changed' :
       return todos. map ( ( t ) => {
         if (t. id === action. todo . id ) {
           return action. todo ;
        } else {
           return t;
        }
      });
    case  'deleted' : {
       return todos. filter ( ( t ) => t. id !== action. id );
    }
    default :
       return todos;
  }
}

export  default  createStore (todoReducer);

// 
import { Provider } from  'react-redux' ;

export  default () => (
   < Provider  store = {store} > 
    < ReduxApp /> 
  </ Provider > 
);

In fact, Contextlooking at the compared versions, you can understand at a glance that what I said Reduxis like a combination of Reducerand , so why use it? ContextLet’s get straight to the reasons:

  1. I wonder Contextif you still remember the shortcomings? Changes in state will cause changes in all components, but Reduxwill only affect components that subscribe to the corresponding state.
  2. I don’t know if you still remember that there was still an unresolved problem in the original version, which was the status update after asynchronous requests. ReduxThere are many useful middlewares to handle these things. For example redux-thunk, of course you can also write it yourself, but what’s the point? ?
  3. Browser plug-in Redux DevToolsallows you to clearly see changes in each status.
  4. It is separated from different UIstate management. For example, if you have multiple applications sharing a set of states at the same time, or two applications you Reactwrite Vueshare a set of states.

This is Reduxthe most powerful advantage I understand, that is, when you don’t have these problems, you don’t need it at all.

Summarize

Like I said, Reduxyou may not need it when you haven’t realized whether you want to use it or not. When you are thinking about how to organize your status, or you are already overwhelmed by your status, you may need it. Think about it, it’s nonsense to talk about technology regardless of requirements. You have enough time to continuously optimize your code instead of integrating all the tools into one application from the beginning, regardless of whether it is real or not. Required, so you’ll never understand the point of using it.