Skip to content

Latest commit

 

History

History

10-use-reducer

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

español

english inglés



10 useReducer

Resume

This example takes as a starting point 09-pure-component-callback.

In the previous sample we worked around the issue with the function that was getting updated on every render by using useCallback, this approach is cool, but for more complex scenarios you may want to organize your code using a different approach. Another way of solving this issue is using useReducer, this hook will return a dispatch function that remains stable.

Steps

  • First we copy the previous example, and do a npm install
npm install
  • Let's open the demo.tsx. We will create a parent that will have information on the first and the last name of a person, and we are going to create a child component that will serve us to edit the name field.

./src/demo.tsx

import React from "react";

interface Props {
  name: string;
  onChange: (value: string) => void;
}

const EditUsername: React.FC<Props> = React.memo((props) => {
  console.log(
    "Hey I'm only rerendered when name gets updated, check React.memo"
  );

  return (
    <input
      value={props.name}
      onChange={(e) => props.onChange(e.target.value)}
    />
  );
});

export const MyComponent = () => {
  const [userInfo, setInfo] = React.useState({ name: "John", lastname: "Doe" });

  return (
    <>
      <h3>
        {userInfo.name} {userInfo.lastname}
      </h3>
      <EditUsername
        name={userInfo.name}
        onChange={(name) =>
          setInfo({
            ...userInfo,
            name,
          })
        }
      />
      <input
        value={userInfo.lastname}
        onChange={(e) =>
          setInfo({
            ...userInfo,
            lastname: e.target.value,
          })
        }
      />
    </>
  );
};
  • In the child component we have a console.log to warn us if the control is repainted or not, this control is always repainted because in the onChange property we are creatin a new function in each render.

Here we might bo more tempted to use React.useCallback, is there antoher way to try this? Let's see the proposal offered by useReducer.

En useReducer we group a set of functionality.

  • On the one hand we have the state (the data).
  • On the other hand we have actions (contains an identifier and a parameter with information) that are launched using a dispatcher.
  • And those actions update the state in a reducer (a reducer is a function that accepts two parameters the previus state and the action, and returns you a new state).
  • Where is this? In thinking that the current state is like a frame of a movie, we fix it, we receive a request for change (with the action) and a new frame is generated based on the previous one and the change that we want to make, if there is no change the same as before is returned.

Let's first to define our reducer.

  • Taking average of the fact that we are working with ** TypeScript ** we are going to type our reducer and actions:

./src/demo.tsx

interface UserState {
  name: string;
  lastname: string;
}

interface Action {
  type: string;
  payload: any;
}

const actionIds = {
  setName: "setname",
  setLastname: "setlastname",
};
  • And now Let's go to create our reducer.

./src/demo.tsx

const userInfoReducer = (state: UserState, action: Action): UserState => {
  switch (action.type) {
    case actionIds.setName:
      return {
        ...state,
        name: action.payload,
      };
    case actionIds.setLastname:
      return {
        ...state,
        lastname: action.payload,
      };
    default:
      return state;
  }
};

Let's now replace the useState of our component with a useReducer Let's see how it looks.

First we add the useReducer

export const MyComponent = () => {
-  const [userInfo, setInfo] = React.useState({ name: "John", lastname: "Doe" });
+  const [userInfo, dispatch] = React.useReducer(userInfoReducer, {name: 'John', lastname: 'Doe'});

On the one and useReducer receives two parameters, the first is the function reducer that we created earlier, and the second is the intial state.

On the other, it returns and array (as in useState), on this array we can do destructuring, on the one hand we bring the photo of the current state in the first element of the array, and for another it gives us a \ dispatcher, this dispatcher acts like a bus, it loads the action we give it and it is carried by the reducer function that updates the state.

Let's go to change the markup of the render and adapt it to use the state.

By having called userInfo to the state we already have, we have saved ourselves work refactoring.

On the other and we are going to change the input that is directly in the parent component.

<input
  value={userInfo.lastname}
-        onChange={e => setInfo({
-          ...userInfo,
-          lastname: e.target.value,
-        })}
+        onChange={(e) => dispatch({type: actionIds.setLastname
+                                  ,payload: e.target.value})}
/>

Look at this change, I no longer directly change the state using the dispatch, I pass the action type that I want to execute, including the data that changes, and this dispatch execute the useReducer function.

Now comes the stronger change, update the child component, in this case we have to change the signature of the properties, we delegate to dispatch the changing information.

This in this example may seem a pointless change, but in one case complex in witch we can have a multitude of callbacks, we save pass them by property, having everything grouped in a single dispatch.

./src/demo.tsx

interface Props {
  name: string;
-  onChange: (value: string) => void;
+  dispatch: React.Dispatch<Action>;
}

const EditUsername: React.FC<Props> = React.memo((props) => {
  console.log(
    "Hey I'm only rerendered when name gets updated, check React.memo"
  );

  return (
    <input
      value={props.name}
-      onChange={(e) => props.onChange(e.target.value)}
+      onChange={(e) =>
+        props.dispatch({ type: actionIds.setName, payload: e.target.value })
+      }
    />
  );
});
  • Let's now update the parent component:

./src/demo.tsx

      <EditUsername
        name={userInfo.name}
-        onChange={(name) =>
-          setInfo({
-            ...userInfo,
-            name,
-          })
+        dispatch={dispatch}
        }
      />

If we execute this example we can see that the problem of rerender, why? Becouse the dispatch function is not regenerated on every render.

Use useReducer in this example has been to killing flies with cannon shots, we have chosen a simple example to be able to learn how it works, what normal is that you use the complex cases where you have a rich state, and a lot of sub-component levels.

useReducer is not a universal solution, and it has its disadventages, you are binding the signature of the properties of your component to a specific dispatch and also launching actions this makes your components less promotables, it is going to be hareder to make them reusables. Yo have to choose ok where to stop using dispatch and use a conventional signature on components that you see that they can be reusable.

About Basefactor + Lemoncode

We are an innovating team of Javascript experts, passionate about turning your ideas into robust products.

Basefactor, consultancy by Lemoncode provides consultancy and coaching services.

Lemoncode provides training services.

For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend