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

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

Related

React Function component setting state variable V/s using local variable

I have a functional component and want to update some state in that component (based on some context API object value i.e. I get via useContext)
Now, I see 2 options;
Using local variable
Using setState
I just wanted to understand the difference between the 2 ways. Also to add, when using setState, I run into below issue
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Just to add, this functional component is a direct child of App component and the context object is updated in another component which is also a direct child of App component.
const MyComponent = () => {
let myTitle = "ABC"; //Option 1 to use 'myTitle'
//const [myTitle, setMyTitle] = useState('ABC'); //Option 2 to use 'myTitle'
if (someContextApiObj.value === "XYZ") {
myTitle = "XYZ";
//setMyTitle("XYZ")
}
return (
<>
<ChildComponent
myTitle={myTitle}
/>
</>
);
};
export default MyComponent;
It seems you are wanting to compute a myTitle value based on some prop or context value being passed to the component. Storing passed props/context values in local state is generally considered anti-pattern in React. Compute the myTitle value and pass it along to the child component.
const MyComponent = () => {
const myTitle = someContextApiObj.value === "XYZ" ? "XYZ" : "ABC";
return <ChildComponent myTitle={myTitle} />;
};
If the myTitle value is an expensive calculation or you just want to provide a stable reference to children, use the useMemo hook to provide this.
useMemo
import { useMemo } from 'react';
const MyComponent = () => {
const myTitle = useMemo(() => {
return someContextApiObj.value === "XYZ" ? "XYZ" : "ABC";
}, [someContextApiObj]);
return <ChildComponent myTitle={myTitle} />;
};
Your code runs into an infinite loop because of your if-clause. If you set the title and the condition doesn't change, it will get set on every rerender causing a new render - infinite chain of rerenders.
Using a state offers you the ability to change the value conditionally, while using a local variable not coupled to a state makes the variable constant.
You should use the Hook useEffect.
const MyComponent = () => {
const [myTitle, setMyTitle] = useState('ABC'); //Option 2 to use 'myTitle'
useEffect(()=> {
if (someContextApiObj.value === "XYZ") {
setMyTitle("XYZ")
}
}, []);
return (
<>
<ChildComponent
myTitle={myTitle}
/>
</>
);
};
export default MyComponent;

Pass function to Context API

I'm dealing with a mix of function components and class components. Every time a click happens in the NavBar I want to trigger the function to validate the Form, it has 5 forms, so each time I'm going to have to set a new function inside the context API.
Context.js
import React, { createContext, useContext, useState } from "react";
const NavigationContext = createContext({});
const NavigationProvider = ({ children }) => {
const [valid, setValid] = useState(false);
const [checkForm, setCheckForm] = useState(null);
return (
<NavigationContext.Provider value={{ valid, setValid, checkForm, setCheckForm }}>
{children}
</NavigationContext.Provider>
);
};
const useNavigation = () => {
const context = useContext(NavigationContext);
if (!context) {
throw new Error("useNavigation must be used within a NavigationProvider");
}
return context;
};
export { NavigationProvider, useNavigation, NavigationContext};
Form.js
import React, { Component } from "react";
import { NavigationContext } from "../hooks/context";
class Something extends Component {
static contextType = NavigationContext;
onClickNext = () => {
// This is the funcion I want to set inside the Context API
if(true){
return true
}
return false;
};
render() {
const { setCheckForm } = this.context;
setCheckForm(() => () => console.log("Work FFS"));
return (
<>
<Button
onClick={this.onClickNext}
/>
</>
);
}
}
export default Something;
The problem when setting the function it throws this error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
And setting like setCheckForm(() => console.log("Work FFS"));, it triggers when rendered.
Render method of React.Component runs whenever state changes and setCheckForm updates the state whenever that render happens. This creates an infinite loop, this is the issue you are having there.
So, this is a lifecycle effect, you have to use that function inside componentDidMount if you want to set it when the component first loads.
While this solves your problem, I wouldn't suggest doing something like this. React's mental model is top to bottom, data flows from parent to child. So, in this case, you should know which component you are rendering from the parent component, and if you know which component to render, that means you already know that function which component is going to provide to you. So, while it is possible in your way, I don't think it is a correct and Reactish way to handle it; and it is probably prone to break.

Avoid re render with react hooks

I have some value that comes from Redux through props and I want to NOT render the component again when this value changes.
I found some answers saying that I can use Memo but I don't know if this is the best option for my case?
My "code":
const MyComponent = () => {
return ...;
}
const mapStateToProps = state => ({
myVar: state.myVar
});
export default connect(mapStateToProps)(MyComponent);
myVar changing shouldn't re render the component in this case.
React.memo can do the job, you can pass a custom equality check function to perform a rerender only when it returns a falsy value. I never faced a case where you want to completely ignore a value update from your Redux store, maybe it shouldn't be stored there ?
Memo API
eg: React.memo(Component, [areEqual(prevProps, nextProps)])
UseSelector API
Another way would be to use useSelector with a custom equality check function:
useSelector Redux API Reference
Connect API
If you still want to stick with mapStateToProps, you can also pass a custom equality check function as a parameter of the connect function:
areStatePropsEqual Redux API Reference
Edit: useRef solution
By using useRef, you store a mutable variable that will be kept as it is for the whole lifetime of the component.
Example based on yours:
const StoreMyVar = (WrappedComponent) => ({myVar, ...props}) => {
const myRefVar = useRef(myVar)
return <WrappedComponent myVar={myRefVar} {...props} />
}
const MyComponentWithImmutableVar = StoreMyVar(MyComponent)
The fastest way is React.memo, but you can use it just with functional components. Be careful, it is not tested.
const MyComponent(props) {
return ...;
}
const areEqual(prevProps, nextProps) {
return prevProps.myVar === nextProps.myvar
}
const mapStateToProps = state => ({
myVar: state.myVar
});
export default React.memo(MyComponent, areEqual);

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.

Trace why a React component is re-rendering

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