Stop re-rendering the react functional component - reactjs

I am using a third party component that re-renders every time state changes which is good but in some instances I do not want it to re-render even if the state changes. Is there a way to do using react functional component. I have read online and it says use shouldComponentUpdate() but I am trying to use functional component and tried using React.Memo but it still re-renders
Code
const getCustomers = React.memo((props) => {
useEffect(() => {
});
return (
<>
<ThirdPartyComponent>
do other stuff
{console.log("Render Again")}
</ThirdPartyComponent>
</>
)
});

For props:
How do I implement shouldComponentUpdate?
You can wrap a function component with React.memo to shallowly compare its props:
const Button = React.memo((props) => {
// your component
});
It’s not a Hook because it doesn’t compose like Hooks do. React.memo is equivalent to PureComponent, but it only compares props. (You can also add a second argument to specify a custom comparison function that takes the old and new props. If it returns true, the update is skipped.)
For state:
There's no build in way to achieve this, but you can try to extract your logic to a custom hook. Here's my attempt to only rerender when shouldUpdate returns true. Use it with caution, because it's the opposite of what React was designed for:
const useShouldComponentUpdate = (value, shouldUpdate) => {
const [, setState] = useState(value);
const ref = useRef(value);
const renderUpdate = (updateFunction) => {
if (!updateFunction instanceof Function) {
throw new Error(
"useShouldComponentUpdate only accepts functional updates!"
);
}
const newValue = updateFunction(ref.current);
if (shouldUpdate(newValue, ref.current)) {
setState(newValue);
}
ref.current = newValue;
console.info("real state value", newValue);
};
return [ref.current, renderUpdate];
};
You would use it like this:
const [count, setcount] = useShouldComponentUpdate(
0,
(value, oldValue) => value % 4 === 0 && oldValue % 5 !== 0
);
In this case, a rerender would occur (due to usages of setcount) if, and only if, shouldUpdate returns true. i.e., when value is multiple of 4 and the previous value is not multiple of 5. Play with my CodeSandbox example to see if that's really what you want.

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.

useEffect is not triggered by a change in ref.current from a child component

I have created a popup that the user can use to add stuff to the application, every field is a separate component, because I need to reuse them in several places in different configruations.
I have tried to create an innerRef that when changed (i.e. new value is typed), the useEffect of the component should be triggered to show or hide the Done button if all values are valid.
I know that all values are valid or not from the valid prop that I assign to .current
export default function AddStock() {
const selectTypeOfQuantityRef = useRef({});
const [allValid, setAllValid] = useState(false);
useEffect(() => {
const allValues = [selectTypeOfQuantityRef.current.valid];
allValues.every((value) => value) ? setAllValid(true) : setAllValid(false);
console.log(allValues.every((value) => value)); // does not get triggered
}, [selectTypeOfQuantityRef.current]);
return (
<>
<AddPopup>
<SelectTypeOfQuantity innerRef={selectTypeOfQuantityRef} />
{allValid && <DoneButton/>}
<CancelButton/>
</AddPopup>
</>
);
}
And this is the select itself (custom of course), that sets innerRef, whenever its state changes.
Everything here works, the state of this small component itself is managed correctly, but it just does not get triggered the state update of the parent component
export default function SelectTypeOfQuantity({ defaultValue = null, innerRef }) {
const [selectTypeOfQuantity, setSelectTypeOfQuantity] = useState(defaultValue);
const [valid, setValid] = useState(false);
const [errMessage, setErrMessage] = useState("Избери стойност!");
useEffect(() => {
innerRef.current.value = selectTypeOfQuantity;
handleValidation(selectTypeOfQuantity);
}, [selectTypeOfQuantity]);
const handleValidation = (value) => {
const result = validateAutocomplete(value);
if (result.valid) {
setValid(true);
setErrMessage(null);
innerRef.current.valid = result.valid;
} else {
setValid(false);
setErrMessage(result.errMessage);
}
};
const selectTypeOfQuantityOnChange = (e, val) => {
setSelectTypeOfQuantity(val ? val.value : null);
};
return (
<Select onChange={selectTypeOfQuantityOnChange}/>
);
}
useRef does not trigger rerenders, thus useEffect will not be called
Use useRef when you need information that is available regardless of component lifecycle and whose changes should NOT trigger rerenders. Use useState for information whose changes should trigger rerenders.
Solution
As React's Philosophy states, all the data must reside within React, that's why even input components are supplied with a value and onChange event. React can't track data changes that happens outside it. As I understand from your question, the changes are happending within the React App, So instead of tracking the data through the innerRef, track them within React using React's own methods.

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;
};

How to compare previous context with current context in React 16.x?

Is there a way to compare previous context value with the current context value in the consumer child component's lifecycle methods?
If there is no way to do that, is there any other workaround or an alternate solution to this?
FYI: I'm using react v16.8.1
Using HOC:
const withContext = Wrapped => props => (
<SomeContext.Consumer>
{value => <Wrapped {...props} contextValue={value}/>}
</SomeContext.Consumer>
);
const MyComponent = withContext(class extends React.Component {
...
componentDidUpdate(prevProps) {
if (prevProps.contextValue !== this.props.contextValue) {
...
}
}
...
});
Using hooks:
function MyComponent(props) {
...
const contextValue = useContext(SomeContext);
const [oldContextValue, saveContextValue] = useState(contextValue);
useEffect(() => {
console.log(oldContextValue, contextValue);
saveContextValue(contextValue);
}, [contextValue]);
...
}
Note that useEffect runs only on the first render and when contextValue changes. So if you don't really need old contextValue for something, you don't need useState.
Usually if you're going to have dynamic context its value will most likely come from a state of a parent component.
So why not detect changes in the state instead of trying to detect changes in the Provider?
Like in the example for dynamic context in the react documentation : https://reactjs.org/docs/context.html#dynamic-context

how can I know what triggered render in React? [duplicate]

Is there a systematic approach to debug what is causing a component to re-render in React? I put a simple console.log() to see how many time it renders, but am having trouble figuring out what is causing the component to render multiple times i.e (4 times) in my case. Is there a tool that exists that shows a timeline and/or all components tree renders and order?
If you want a short snippet without any external dependencies I find this useful
componentDidUpdate(prevProps, prevState) {
Object.entries(this.props).forEach(([key, val]) =>
prevProps[key] !== val && console.log(`Prop '${key}' changed`)
);
if (this.state) {
Object.entries(this.state).forEach(([key, val]) =>
prevState[key] !== val && console.log(`State '${key}' changed`)
);
}
}
Here is a small hook I use to trace updates to function components
function useTraceUpdate(props) {
const prev = useRef(props);
useEffect(() => {
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
if (prev.current[k] !== v) {
ps[k] = [prev.current[k], v];
}
return ps;
}, {});
if (Object.keys(changedProps).length > 0) {
console.log('Changed props:', changedProps);
}
prev.current = props;
});
}
// Usage
function MyComponent(props) {
useTraceUpdate(props);
return <div>{props.children}</div>;
}
You can check the reason for a component's (re)render with the React Devtools profiler tool. No changing of code necessary. See the react team's blog post Introducing the React Profiler.
First, go to settings cog > profiler, and select "Record why each component rendered"
Here are some instances that a React component will re-render.
Parent component rerender
Calling this.setState() within the component. This will trigger the following component lifecycle methods shouldComponentUpdate > componentWillUpdate > render > componentDidUpdate
Changes in component's props. This will trigger componentWillReceiveProps > shouldComponentUpdate > componentWillUpdate > render > componentDidUpdate (connect method of react-redux trigger this when there are applicable changes in the Redux store)
calling this.forceUpdate which is similar to this.setState
You can minimize your component's rerender by implementing a check inside your shouldComponentUpdate and returning false if it doesn't need to.
Another way is to use React.PureComponent or stateless components. Pure and stateless components only re-render when there are changes to it's props.
#jpdelatorre's answer is great at highlighting general reasons why a React component might re-render.
I just wanted to dive a little deeper into one instance: when props change. Troubleshooting what is causing a React component to re-render is a common issue, and in my experience a lot of the times tracking down this issue involves determining which props are changing.
React components re-render whenever they receive new props. They can receive new props like:
<MyComponent prop1={currentPosition} prop2={myVariable} />
or if MyComponent is connected to a redux store:
function mapStateToProps (state) {
return {
prop3: state.data.get('savedName'),
prop4: state.data.get('userCount')
}
}
Anytime the value of prop1, prop2, prop3, or prop4 changes MyComponent will re-render. With 4 props it is not too difficult to track down which props are changing by putting a console.log(this.props) at that beginning of the render block. However with more complicated components and more and more props this method is untenable.
Here is a useful approach (using lodash for convenience) to determine which prop changes are causing a component to re-render:
componentWillReceiveProps (nextProps) {
const changedProps = _.reduce(this.props, function (result, value, key) {
return _.isEqual(value, nextProps[key])
? result
: result.concat(key)
}, [])
console.log('changedProps: ', changedProps)
}
Adding this snippet to your component can help reveal the culprit causing questionable re-renders, and many times this helps shed light on unnecessary data being piped into components.
Strange nobody has given that answer but I find it very useful, especially since the props changes are almost always deeply nested.
Hooks fanboys:
import deep_diff from "deep-diff";
const withPropsChecker = WrappedComponent => {
return props => {
const prevProps = useRef(props);
useEffect(() => {
const diff = deep_diff.diff(prevProps.current, props);
if (diff) {
console.log(diff);
}
prevProps.current = props;
});
return <WrappedComponent {...props} />;
};
};
"Old"-school fanboys:
import deep_diff from "deep-diff";
componentDidUpdate(prevProps, prevState) {
const diff = deep_diff.diff(prevProps, this.props);
if (diff) {
console.log(diff);
}
}
P.S. I still prefer to use HOC(higher order component) because sometimes you have destructured your props at the top and Jacob's solution doesn't fit well
Disclaimer: No affiliation whatsoever with the package owner. Just clicking tens of times around to try to spot the difference in deeply nested objects is a pain in the.
Using hooks and functional components, not just prop change can cause a rerender. What I started to use is a rather manual log. It helped me a lot. You might find it useful too.
I copy this part in the component's file:
const keys = {};
const checkDep = (map, key, ref, extra) => {
if (keys[key] === undefined) {
keys[key] = {key: key};
return;
}
const stored = map.current.get(keys[key]);
if (stored === undefined) {
map.current.set(keys[key], ref);
} else if (ref !== stored) {
console.log(
'Ref ' + keys[key].key + ' changed',
extra ?? '',
JSON.stringify({stored}).substring(0, 45),
JSON.stringify({now: ref}).substring(0, 45),
);
map.current.set(keys[key], ref);
}
};
At the beginning of the method I keep a WeakMap reference:
const refs = useRef(new WeakMap());
Then after each "suspicious" call (props, hooks) I write:
const example = useExampleHook();
checkDep(refs, 'example ', example);
Thanks to https://stackoverflow.com/a/51082563/2391795 answer, I've come up with this slightly different solution for Functional components only (TypeScript), which also handles states and not only props.
import {
useEffect,
useRef,
} from 'react';
/**
* Helps tracking the props changes made in a react functional component.
*
* Prints the name of the properties/states variables causing a render (or re-render).
* For debugging purposes only.
*
* #usage You can simply track the props of the components like this:
* useRenderingTrace('MyComponent', props);
*
* #usage You can also track additional state like this:
* const [someState] = useState(null);
* useRenderingTrace('MyComponent', { ...props, someState });
*
* #param componentName Name of the component to display
* #param propsAndStates
* #param level
*
* #see https://stackoverflow.com/a/51082563/2391795
*/
const useRenderingTrace = (componentName: string, propsAndStates: any, level: 'debug' | 'info' | 'log' = 'debug') => {
const prev = useRef(propsAndStates);
useEffect(() => {
const changedProps: { [key: string]: { old: any, new: any } } = Object.entries(propsAndStates).reduce((property: any, [key, value]: [string, any]) => {
if (prev.current[key] !== value) {
property[key] = {
old: prev.current[key],
new: value,
};
}
return property;
}, {});
if (Object.keys(changedProps).length > 0) {
console[level](`[${componentName}] Changed props:`, changedProps);
}
prev.current = propsAndStates;
});
};
export default useRenderingTrace;
Note the implementation itself hasn't changed much. The documentation shows how to use it for both props/states and the component is now written in TypeScript.
The above answers are very helpful, just in case if anyone is looking for a specfic method to detect the cause of rerender then I found this library redux-logger very helpful.
What you can do is add the library and enable diffing between state(it is there in the docs) like:
const logger = createLogger({
diff: true,
});
And add the middleware in the store.
Then put a console.log() in the render function of the component you want to test.
Then you can run your app and check for console logs.Wherever there is a log just before it will show you difference between state (nextProps and this.props) and you can decide if render is really needed there
It will similar to above image along with the diff key.

Resources