Redux’s three principles

The development and use of Redux must follow three principles:

  1. Single data source: The state of the entire application is stored in an object tree that exists in a single store
  2. State is read-only: the only way to change State is to trigger an action, which is a generic object that describes events that have occurred.
  3. Use pure functions to perform modifications: To describe how an action changes the State Tree, you need to write reducers

On the first point, it is easy to understand that there should be only one store for the entire app. A globally unique store helps to better manage the state of the whole app, facilitates development and debugging, and makes it easier to implement “undo”, “redo” and other functions.

Second, state is read-only. Therefore, we should not modify state directly at any time. The only way to change state is to dispatch an action to modify state indirectly, so as to ensure effective management of state of large applications.

Third, to modify state, we must compile a reducer, which must be a pure function. Reducer receives the previous state and action and returns a new state.

What is a pure function?

As mentioned earlier when we introduced the three principles of REdux, reducer must be compiled to modify state, and reducer must be a pure function, then the question comes, what is a pure function?

Wikipedia defines a pure function as follows:

In programming, a function may be considered pure if it meets the following requirements:

  • This function needs to produce the same output at the same input value. The output of a function is independent of any hidden information or state other than the input value, or of the external output generated by the I/O device.
  • This function cannot have semantically observable function side effects, such as “firing events,” making output devices output, or changing the contents of objects other than output values.

To summarize, if a function returns a result that depends only on its arguments and has no side effects during execution, we define the function as pure.

An 🌰 :

const x = 1;
function add(a) {
  return a + x;
}
foo(1); / / 2
Copy the code

Add is not a pure function because the return value of add depends on the external variable X, and the output is uncertain at certain inputs.

Let’s tweak the above example slightly:

const x = 1;
function add(a, b) {
  return a + b;
}
foo(1.2); / / 3
Copy the code

The function is now a pure function, because the return value of add is always dependent on its input parameters A and B, regardless of how the external variable x changes.

Here’s another example:

function add(obj, a) {
  obj.x = 1;
  return obj.x + a;
}
const temp = {
  x: 4.y: 9,
}
add(temp, 10); / / 11
console.log(temp.x); / / 1
Copy the code

Now, we pass an object to add, and we modify some properties of the object inside add. When we execute add, we modify the temp object passed in from the outside, which has the side effect, so it is not a pure function.

In addition to the above mentioned, external variables cannot be modified inside a pure function. Calling the Dom API inside a function to modify a page, sending Ajax requests, or even calling console.log to print a log are all side effects, which are prohibited in pure functions. Do not rely on data other than function parameters when calculating.

Why does the Reducer need to return a new state

Above we have introduced what is a pure function. In Redux, reducer must be a pure function, and each pure function needs to return a new state. Therefore, there must be a question why reducer must return a new state. Can’t I just change the state and return it?

With this problem in mind, let’s take an example to verify what happens if we directly modify the state value in a Reducer and then return the modified state.

We define three components: App, Title, and Content. App, as the parent of Title and Content, has a default state tree, structured as follows:

Initial state:

{
  book: {
    title: {
      tip: 'I am the title'.color: 'red',},content: {
      tip: 'I am content'.color: 'blue',}}}Copy the code

The Title component:

const Title = ({ title }) = > {
   console.log('render Title');
   return <div style={{ color: title.color}} >{title.tip}</div>;
}
Copy the code

The Content components:

const Content = ({ content }) = > {
   console.log('render Content');
   return <div style={{ color: content.color}} >{content.tip}</div>;
}
Copy the code

App component:

const App = ({ book, dispatch }) = > {
  const changeTitleTip = () = > {
    dispatch({
      type: 'book/changeTitleTip'.payload: {
        title: {
          tip: 'Modified title'.color: 'green',}}}); };console.log('render App');
  return (
    <div>
      <Button onClick={changeTitleTip}>Changing the title Name</Button>
      <Title title={book.title} />
      <Content content={book.content} />
    </div>
  );
};
Copy the code

Reducer:

reducers: {
  changeTitleTip(state, { payload }) {
    const { title } = payload;
    state.title = title;
    returnstate; }},Copy the code

The demo is pretty simple. We’re going toAppComponent triggers adispatch, send aaction, the callreducerTo modify thestateThe inside of thetitle, we click the button of “Modify title Name”, and find that the component does not change as we expected. However, by checking the data in state, we find that the value of state has changed.The page did not change as expected:This is a good examplereduxWe can’t modify it directlystateAnd return.

Now adjust reducer, through… The operator creates a new object and copies all the properties of state into the new object. We forbid direct modification of the original object. Once you want to modify some properties, you must copy all the objects in the modification path.

state.title.text = 'hello'
Copy the code

Instead, we create a new state, state.title, state.title.tip. The advantage of this is that you can implement objects with shared structures.

For example, state and newState are two different objects, and the content property in these two objects does not need to be changed in our scenario, so the content property can refer to the same object, but since the title is overwritten by a new object, So their title property points to different objects,

Using a tree structure to represent the object structure, the structure would look like this:

Reducer now:

reducers: {
  changeTitleTip(state, { payload }) {
    const { title } = payload;
    let newState = { // Create a newState. state,// Copy the contents of state
	  title: { // Override the original title property with a new object. state.title,// Copy the contents of the original title object
	    tip: 'hello' // Override the tip attribute}}returnnewState; }}Copy the code

Click the “Change title name” button again, and the desired effect will be achieved.All right, now that we know the results let’s delve a little bit into the reasons behind this.

View the combineReducers source code for Redux

let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
  const key = finalReducerKeys[i]
  const reducer = finalReducers[key]
  const previousStateForKey = state[key]
  const nextStateForKey = reducer(previousStateForKey, action)
  if (typeof nextStateForKey === 'undefined') {
    const actionType = action && action.type
    throw new Error(
      `When called with an action of type ${
        actionType ? `"The ${String(actionType)}"` : '(unknown type)'
      }, the slice reducer for key "${key}" returned undefined. ` +
        `To ignore an action, you must explicitly return the previous state. ` +
        `If you want this reducer to hold no value, you can return null instead of undefined.`) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey ! == previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length ! = =Object.keys(state).length
return hasChanged ? nextState : state
Copy the code

We found that the internal through hasChanged combineReducers = hasChanged | | nextStateForKey! == previousStateForKey is used to compare whether the old and new objects are the same, to determine whether to return nextState or state. For performance reasons, redux directly uses shallow comparison, that is, to compare the reference addresses of the two objects, so, When the Reducer function returns the old state object directly, the shallow comparison here fails and Redux thinks nothing has changed, causing some unexpected things to happen with the page update.

immer

We have analyzed above why reducer in REdux should return a new state. However, if reducer is written according to the above method, it will be very troublesome to modify the state tree after it has a deep level. Is there a quick way to modify state directly?

The answer is yes.

Immer is an IMmutable library written by the author of Mobx. The core implementation is to use ES6 proxy to implement JS immutable data structure with minimal cost. It is easy to use, small size, ingenious design, and meets our needs for JS immutable data structure. Of course, there are other libraries besides IMmer that can also solve our problem, but immer is probably one of the simplest and easiest libraries to use.

If your project uses DVA, you can simply turn on DVA-IMMER to simplify state writing. The above example could be written like this:

reducers: {
  changeTitleTip(state, { payload }) {
    const{ title } = payload; state.title = title; }}Copy the code

Or directly use the IMmer library to improve our reducer compilation:

Installation:

yarn add immer
Copy the code

Use:

import produce from "immer";

const reducer = (state, action) = > produce(state, draft= > {
  const { title } = payload;
  draft.title = title;
});
Copy the code

conclusion

This article focuses on redux concepts, what is a pure function, and why reducer needs to return a new state. Finally, imMER library is introduced, and the concept of IMMUTABLE is introduced. Redux and IMMER can facilitate us to use Redux easily and efficiently.