React Hooks, how to prevent unnecessary rerendering - reactjs

I have a hook that gets data from another hook
const useIsAdult = () => {
const data = useFormData();
return data.age > 18;
}
This hook returns true or false only, however the useFormData is constantly being updated. Everytime useFormData reruns, it will rerender my component. I only want my component to rerender if the result of useIsAdult changes.
I know this can obviously be solved by implementing some logic via react-redux and using their useSelector to prevent rendering, but I'm looking for a more basic solution if there is any.
Before you say useMemo, please read question carefully. The return value is a boolean, memo here doesnt make any sense.

Even with returned useMemo in useIsAdult parent component will be rerendered. This problem is reason why I rarely create custom hooks with other hooks dependency. It will be rerender hell.
There I tried to show that useMemo doesnt work. And using useMemo for components its wrong way. For components we have React.memo.
const MyComponent = memo(({ isAdult }: { isAdult: boolean }) => {
console.log("renderMy");
return <h1>Hello CodeSandbox</h1>;
});
And memo will help to prevent rendering of MyComponent. But parent component still render.
https://codesandbox.io/s/interesting-lumiere-xbt4gg?file=/src/App.tsx
If you can modify useFormData maybe useRef can help. You can store data in useRef but it doesnt trigger render.
Something like this but in useFormData:
onst useIsAdult = () => {
const data = useFormData();
const prevIsAdultRef = useRef();
const isAdult = useMemo(() => data.age > 18, [data]);
if (prevIsAdultRef.current === isAdult) {
return prevIsAdultRef.current;
}
prevIsAdultRef.current = isAdult;
return isAdult;
};
I dont know how useFormData works, then you can try it self with useRef.
And if useFormData gets data very often and you cant manage this, you can use debounce or throttle to reduce number of updates

Memoize hook
const useIsAdult = () => {
const data = useFormData();
return useMemo(() => data.age > 18, [data.age]);
}
Here, useMemo will let you cache the calculation or multiple renderes, by "remembering" previous computation. So, if the dependency (data.age) doesn't change then it will use simply reuse the last value it returned, the cached one.
Memoize component expression
useMemo can also be used to memoize component expressions like this: -
const renderAge = useMemo(() => <MyAge age={data.age} />, [data.age]);
return {renderAge}
Here, MyAge will only re-render if the value of data.age changes.

Related

Which is the correct way to handle values props before charge component in React?

i have a doubt about React hook, his props and useEffect.
In my component, i receive a list inside props, i must filter the received list and get a value from the list before the component is mounted.
(this code is a simplified example, i apply another filters and conditions more complex)
function MyComponent(props) {
const [value, setValue] = useState(null)
useEffect(()=>{
var found = props.myList.find((x)=> {return x.name=="TEST"});
if(found.length > 0)
setValue("Hello World")
}, []);
return (
<div>{value}</div>
)
}
Is correct to use useEffect for get and set a value respect to props before the component is mounted?
This useEffect is executed after the 1st render, so null would be "rendered" first, and then the useEffect would update the state causing another render.
However, if your value is dependent on the props, and maybe on other states (filters for example), this is a derived value, and it doesn't need to be used as a state. Instead calculate it on each render, and if it's a heavy computation that won't change on every render wrap it with useMemo:
function MyComponent({ myList }) {
const value = useMemo(() => {
const found = myList.some(x => x.name=="TEST");
return found ? 'Hello World' : '';
}, [myList]);
return (
<div>{value}</div>
)
}
There's no need to use useEffect here, you can simply pass a callback to the useState hook. The callback is only ran on the initialization of the hook so this has the same outcome as your useEffect hook (i.e. it doesn't run on every rerender so same performance hit).
function MyComponent({ myList }) {
const [value, setValue] = useState(() => {
const found = myList.find(x => x.name === 'TEST');
return found ? 'Hello World' : null;
});
return (
<div>{value}</div>
);
}
No need for any hook at all, actually, you should just write it like this:
function MyComponent({ myList }) {
const found = myList.some(x => x.name == "TEST");
const value = found ? 'Hello World' : '';
return (
<div>{value}</div>
)
}
Using useEffect or even only useState is conceptually wrong. The useState hook is used when a component needs to have its own independent internal state which isn't the case here, as the value is purely derived from the props. The useEffect hook is an escape hatch typically used when you need to sync with some external imperative API, not at all the case here.
The solution with useMemo is perfectly fine, and more efficient if your filtering condition is very computationally expensive, but it is also a lot more complicated to understand and to maintain, so I would suggest going for the simplest solution until the lack of memoization actually becomes a measurable problem.

Prevent re-render while using custom hooks

I'm experiencing something similar to this:
Should Custom React Hooks Cause Re-Renders of Dependent Components?
So, I have something like this:
const ComponentA = props => {
const returnedValue = useMyHook();
}
And I know for a fact that returnedValue is not changing (prev. returnedValue is === to the re-rendered returnedValue), but the logic inside of the useMyHook does cause internal re-renders and as a result, I get a re-render in the ComponentA as well.
I do realize that this is intentional behavior, but what are my best options here? I have full control over useMyHook and returnedValue. I tried everything as I see it, caching(with useMemo and useCallback) inside of useMyHook on returned value etc.
Update
Just to be more clear about what I'm trying to achieve:
I want to use internal useState / useEffect etc inside of useMyHook and not cause re-render in the ComponentA
Try to find out the reason of re-rendering in your hook, probably caused due to a state update. If it is due to updation of states in useEffect then give the states and values as dependency. If your states are updating in a function, do try to call the function in order to invoke.
If these doesn't work, try to use ref. useRef hook can be used to prevent such re-renders as it will provide you with .current values.
It would be better if you remove your useEffect dependencies one by one so that you can know what dependency is causing the error and create a ref for the same.
You can share a codesandbox and I would be happy to help you out with it.
I want to use internal useState / useEffect etc inside of useMyHook and not cause re-render in the ComponentA
If you want to avoid the renders triggered by useState() or useEffect() then they are not the right tools.
If you need to hold some mutable state without triggering renders then consider using useRef() instead.
This question has a nice comparison of the differences between useState() and useRef().
Here is a simple example:
const ComponentState = () => {
const [value, setValue] = useState(0);
return (<>
<button onClick={() => {
// Will trigger render
setValue(Math.random());
}></button>
</>);
}
const ComponentRef = () => {
const valueRef = useRef(0);
return (<>
<button onClick={() => {
// Will not trigger render
valueRef.current = Math.random();
}></button>
</>);
}

React useCallback with onClick not working. Rerenders child component

TimeChild re-renders in below image even after using useCallback
When Time sets state, then Time is going to rerender. Then, unless you do something to stop it, all of its children will rerender too. If you want to stop a child from rerendering, then the child component needs to use React.memo.
const TimeChild = React.memo(() => {
// ...
});
If you do this, then when TimeChild would render, it will first do an === comparison between each of its old props and each of its new props. If they are all the same, TimeChild will skip rendering.
The only role that useCallback plays in this is if TimeChild receives a function as a prop. If it does, then you need to make sure it receives the same function each time, or React.Memo will never be able to skip rendering because its props keep changing. But in your example there are no props at all being passed to TimeChild, so useCallback is not necessary.
You can use 'useCallback' in this way :
import React, { useCallback, useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
const callBckValue = useCallback(() => {
setCount((count) => count + 1);
}, []);
return (
<div>
<h2>{count}</h2>
<button type="button" onClick={callBckValue}>
Click Me
</button>
</div>
);
};
export default App;
Firstly, you need to be aware, only in special situation, it makes sense to stop child component from re-rendering. If your case is not that special, that might not be a good idea.
Secondly, if you are sure you have to do it, use React.memo, the usage is pretty like componentShouldUpdate in class component

Mixing Redux with useEffect Hook

I read that this is theoretically OK. In my small use case, I'm running into an issue, where mixing those technologies leads to double re-rendering.
First, when redux dispatch is executed and some components use a prop via useSelector. Then, after the functional component is already re-rendered, a useEffect hook is being applied which updates the state of some property. This update re-triggers the render again.
E.g. the below console log prints out twice in my case.
Question: should I remove the useEffect and useState and integrate it into redux' store?
import {useSelector} from "react-redux";
import React from "react";
const Dashboard = (props) => {
const selFilters = useSelector((state) => state.filter.selectedDashboardFilters);
const [columns, setColumns] = React.useState([]);
React.useEffect(() => {
let newColumns = getColumns();
setColumns(newColumns)
}, [selFilters]
)
console.log("RENDER")
return (
<h1>{columns.length}</h1>
)
}
If columns needs to be recomputed whenever selFilters changes, you almost certainly shouldn't be recomputing it within your component. If columns is computed from selFilters, then you likely don't need to store it as state at all. Instead, you could use reselect to create a getColumns() selector that derives the columns from the state whenever the relevant state changes. For example:
const getColumns = createSelector(
state => state.filter.selectedDashboardFilters,
selFilters => {
// Compute `columns` here
// ...
return columns
}
)

More advanced comparison in React's useEffect

I am looking for a way to perform more advanced comparison instead of the second parameter of the useEffect React hook.
Specifically, I am looking for something more like this:
useEffect(
() => doSomething(),
[myInstance],
(prev, curr) => { /* compare prev[0].value with curr[0].value */ }
);
Is there anything I missed from the React docs about this or is there any way of implementing such a hook on top of what already exists, please?
If there is a way to implement this, this is how it would work: the second parameter is an array of dependencies, just like the useEffect hook coming from React, and the third is a callback with two parameters: the array of dependencies at the previous render, and the array of dependencies at the current render.
you could use React.memo function:
const areEqual = (prevProps, nextProps) => {
return (prevProps.title === nextProps.title)
};
export default React.memo(Component, areEqual);
or use custom hooks for that:
How to compare oldValues and newValues on React Hooks useEffect?
In class based components was easy to perform a deep comparison. componentDidUpdate provides a snapshot of previous props and previous state
componentDidUpdate(prevProps, prevState, snapshot){
if(prevProps.foo !== props.foo){ /* ... */ }
}
The problem is useEffect it's not exactly like componentDidUpdate. Consider the following
useEffect(() =>{
/* action() */
},[props])
The only thing you can assert about the current props when action() gets called is that it changed (shallow comparison asserts to false). You cannot have a snapshot of prevProps cause hooks are like regular functions, there aren't part of a lifecycle (and don't have an instance) which ensures synchronicity (and inject arguments). Actually the only thing ensuring hooks value integrity is the order of execution.
Alternatives to usePrevious
Before updating check if the values are equal
const Component = props =>{
const [foo, setFoo] = useState('bar')
const updateFoo = val => foo === val ? null : setFoo(val)
}
This can be useful in some situations when you need to ensure an effect to run only once(not useful in your use case).
useMemo:
If you want to perform comparison to prevent unecessary render calls (shoudComponentUpdate), then useMemo is the way to go
export React.useMemo(Component, (prev, next) => true)
But when you need to get access to the previous value inside an already running effect there is no alternatives left. Cause if you already are inside useEffect it means that the dependency it's already updated (current render).
Why usePrevious works
useRef isn't just for refs, it's a very straightforward way to mutate values without triggering a re render. So the cycle is the following
Component gets mounted
usePrevious stores the inital value inside current
props changes triggering a re render inside Component
useEffect is called
usePrevious is called
Notice that the usePrevious is always called after the useEffect (remember, order matters!). So everytime you are inside an useEffect the current value of useRef will always be one render behind.
const usePrevious = value =>{
const ref = useRef()
useEffect(() => ref.current = value,[value])
}
const Component = props =>{
const { A } = props
useEffect(() =>{
console.log('runs first')
},[A])
//updates after the effect to store the current value (which will be the previous on next render
const previous = usePrevious(props)
}
I hit the same problem recently and a solution that worked for me is to create a custom useStateWithCustomComparator hook.
In your the case of your example that would mean to replace
const [myInstance, setMyInstance] = useState(..)
with
const [myInstance, setMyInstance] = useStateWithCustomComparator(..)
The code for my custom hook in Typescript looks like that:
const useStateWithCustomComparator = <T>(initialState: T, customEqualsComparator: (obj1: T, obj2: T) => boolean) => {
const [state, setState] = useState(initialState);
const changeStateIfNotEqual = (newState: any) => {
if (!customEqualsComparator(state, newState)) {
setState(newState);
}
};
return [state, changeStateIfNotEqual] as const;
};

Resources