Prevent Unnecessary Rendering When Using React Hooks

Jul 15th, 2022 - written by Kimserey with .

When working with state hooks like useReducer or useState or useContext, it can become expensive to render components. If we let the framework handle rendering, all components and children components will be rendered if the parent uses state or context hooks or an action is dispatch into the reducer hook. Today we will look at optimisation to stop the rendering propagation for components which do not need to be rendered.

Localise State

The first technique is to organise state update to be localised into components that need updates. We can prevent rendering of all children elements by removing state change from the parent.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const MyChildComp: FC<{ toggled: boolean; onClick: () => void }> = ({
toggled,
onClick,
}) => {
return (
<>
<div>{toggled ? "hello" : "bye"}</div>
<button onClick={() => onClick()}>Click</button>
</>
);
};

const MyParentComp: FC = () => {
const [state, setState] = useState(true);

return <MyChildComp toggled={state} onClick={() => setState(!state)} />;
};


If we have a state located in the parent component but is being used within the child component, any changes of the state will trigger a render of both components and any other children components of the parent component even if they don’t need the state.

Identifying such pattern can help in reducing rendering by moving the state closer to where it gets mutated, which mean in the example above, moving the state hook inside the child component rather than having it on the parent.

Memo

But there are times where it is better to keep the state at the parent level and breakdown parts of the state to display in children components.

In that case any update will trigger a render of all components. To prevent that, we can use memo. This will make sure that the component is only rendered if the props have changed.

For example in a reducer hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
interface CounterState {
count: number;
name: string;
}

interface IncrementCounter {
kind: "Increment";
}

interface ChangeName {
kind: "ChangeName";
name: string;
}

type CounterAction = IncrementCounter | ChangeName;

const reducer = (state: CounterState, action: CounterAction): CounterState => {
switch (action.kind) {
case "Increment":
return { count: state.count + 1, name: state.name };
case "ChangeName":
return { count: state.count, name: action.name };
}
};

const DispatcherContext = React.createContext<
{ dispatch: Dispatch<CounterAction> } | undefined
>(undefined);

const ButtonComponent: FC = () => {
const dispatcher = useContext(DispatcherContext);

return (
<div>
<button onClick={() => dispatcher?.dispatch({ kind: "Increment" })}>
Increment
</button>
<button
onClick={() =>
dispatcher?.dispatch({ kind: "ChangeName", name: "Hello " })
}
>
Change name
</button>
</div>
);
};

const NameDisplay: FC<{ name: string }> = ({ name }) => {
return <>{name}</>;
};

const App: FC = () => {
const initialState = {
count: 0,
name: "",
};

const [state, dispatch] = useReducer(reducer, initialState);

return (
<DispatcherContext.Provider value=>
<ButtonComponent></ButtonComponent>
<p>{state.count}</p>
<NameDisplay name={state.name} />
</DispatcherContext.Provider>
);
};


We have a reducer which keeps track of a counter with name and we have NameDisplay, a component which should render only if the state.name has changed. Currently any change on the state will trigger a render. To prevent that, we can use memo.

1
2
3
const NameDisplay: FC<{ name: string }> = memo(({ name }) => {
return <>{name}</>;
});


Memo With Change Function

Lastly when managing a large state object with reducer, it is sometime easier to pass as prop the whole state. For example instead of passing just the name, we pass the whole state:

1
2
3
const NameDisplay: FC<{ state: CounterState }> = memo(({ state }) => {
return <>{state.name}</>;
});


If we forward the whole state as prop, even when using memo, the components will still render.

To prevent this, we can use the equality argument from usememo to identify the part of the state that the component cares about and which would trigger rendering.

1
2
3
4
5
6
const NameDisplay: FC<{ state: CounterState }> = memo(
({ state }) => {
return <>{state.name}</>;
},
(cur, next) => cur.state.name === next.state.name
);


And that concludes today’s post!

Conclusion

Today we saw how we could optimise our components to avoid unnecessary rendering. We first saw that by simply reorganising the state, we can remove the need to render certain component. We then saw that by use memo we were able to make the component only render on prop change and lastly if not all values from an object give in prop is needed, we saw that we could use the equality argument to target the exact properties from the prop object. I hope you liked this post and I’ll see you on the next one!

Designed, built and maintained by Kimserey Lam.