Solving common problems, performance and scalability issues with react hooks
I will cover most commonly used hooks:
- useState
- useEffect
- useReduce
- useContext
- useMemo
useState
This is the most commonly used hook, as simple as you think it’s made of two parts, a variable and a function to update it
As you can see I have intialized a variable fontSize with a value of 14, and updateFontSize will be used to update it everytime, for more detail on updating the fontSize, check office react docs https://reactjs.org/docs/hooks-state.html
However, for pretty simple few liner apps, using useState everywhere for each new state variable is ok, but problem starts when things starts to get complex and you have multiple child components and further grand children as well, to update the state variable on an event fired from n layer of child component, you need to uplift the state at multiple layer,
Although there are no performance harms in this, Even having separate state variable for everything can boost your performance, as on each update, react will re-render only a smaller sub part of the tree.
But scalability will be an issue here, so here comes our second hook to the rescue ‘useContext’
useContext
useContext was created on the purpose to provide state update on multiple layers of children components
As you can see from above example, we are able to drill down the state till 3rd layer and more is possible,
But what’s the catch here? the assumption here is that we only have one variable yet, so it is still a pretty simple app, but what about if your parent component grow in size and there are multiple variables floating everywhere, how do you keep track of them?
Also with multiple state update from everywhere, if we have any heavy calculation at any place, whole component’s performance will suffer.
for e.g.
as you can see, state is being accessed and updated from multiple locations, also our useState variables are growing in numbers, but there are two bottlenecks here
- If any component inside the Root context provider, update a state value, whole App component will re-render, and block javascript main thread for further execution , hence our transitions will not be smooth
- When our state is updated, will cause to calculate each previous value, which is not needed
So for the first problem, simplest approach is to divide the contexts into smaller contexts, so only a singular part of main component will re-rendered ,
well, our first issue is solved, before solving the second issue , let’s do one more thing, remove all useState variable, and create a singular state, and dispatch action like redux pattern, but without redux,
by doing this, we will achieve scalability, readability and predictability of update of our state in complex apps. To do this , we need our third hook ‘useReducer’
useReducer
useReducer is like useState, just that it hold multiple state variables, and allow a single dispatch action to update the state in immutable fashion, means creating a copy of older state each time before updating
useReducer function takes two main params, one is update function, second is initial parameters, let’s modify our original code further
I will define my actions as ‘UPDATE_FONT_SIZE’, “UPDATE_FONT_FAMILY” and “UPDATE_TEXT” respectively.
as you can see, already our code looks much better, one thing to keep in mind here is , you can have multiple useReducer for multiple context, but don’t do it until they make sense logically, here I am using single state variable for both my context providers.
now our last issue is remaining , which we solve it with ‘useMemo’
useMemo
When our state is updated, will cause to calculate each previous value, which is not needed
to do this we use a famous concept memoization , where last calculated value is cached for further uses. To achieve this we have ‘useMemo’ hook, all we need to do is to wrap our context variables inside useMemo, so it will update only part of context object, which are actually updated and retain all previous calculations, so if value is not changed, it will not update the component at all.
All our major bottlenecks should be solved untill now, but how do ‘useEffect’ comes into the picture, which is the most important of all
useEffect
There are two purpose of useEffect,
- as a replacement of ‘didUpdate’ lifecycle: if you need to call an ajax or load something as a sideeffect, you do it here
useEffect(() => {
(async () => { //fetch something //update State variable after fecth completes
})();
}, [])
as you can see, we used an IIFE inside useEffect to call an fetch api, because it’s async in nature, and useEffect for now doesn’t expect the callback to return a promise, instead it can return a function only, but what does the second array parameter do here, let’s take a look in second step
2. Second use of useEffect, is to do an action on updating the useState variable, this comes in handy when you need to do something as soon as a state variable update
//e.g.
const [data, updateData] = useState([])//pushData
updateData(data => [...data, {name: 'abc'});
fireVal(data[0].name);
the above example is unpredictable in nature, as updateData might not be finished before ‘fireVal’ function is called expect a new data value at index 0,
most common approach which comes to your mind is to use an await
//e.g.
const [data, updateData] = useState([])//pushData
await updateData(data => [...data, {name: 'abc'});
fireVal(data[0].name);
but it doesn’t work either as useState doesn’t provide a callback or promise to work with,
the only solution we have is to use a conditional useEffect, which fires only when updateData is called
//e.g.
const [data, updateData] = useState([])//pushData
updateData(data => [...data, {name: 'abc'});useEffect(() => {fireVal(data[0].name)}, [data]);
Voila, and it does work, it actually waits till component re-render on data variable hence useEffect is called, where you can do whatever you want with new data value,
but here can be the problem of infinite render, if you try to update the same useState variable from inside the useEffect
//infinite loop, because you are updating the state variable again from inside useEffect
useEffect(() => {fireVal(data[0].name); setData([])}, [data]);
even if you are mandate to do it, have an if condition to avoid it
//infinite loop, because you are updating the state variable again from inside useEffect
useEffect(() => {
if(data.length) {
fireVal(data[0].name);
setData([])
}
}, [data]);
hence , second time useEffect won’t be called because data is empty again.
This was a long read, but can help you avoid many obstacles once you start development.