I have a wrapping parent component that accepts some layout-related props, and a child component (representing a page) that needs to run a GraphQL query via Apollo, and update that parent's prop with the result (say the page title). The parent requiring props that I need to update is not a pattern I chose, but the implementation of a component I must use.
App = () {
const [layout, setLayout] = useState({});
return (
<Wrapper layout={layout: 'foo'}>
<PageComponent setLayout={setLayout} />
</Wrapper>
);
}
PageComponent = (props) => {
const { loading, error, data } = useQuery(QUERY); // Apollo's useQuery
useEffect(() => {
props.setPageParams({ title: data.lists[0].title });
}, [data]);
if (loading) return <p>Loading</p>;
if (error) return <p>Error</p>;
return (
<p>component!</p>
);
}
I've tried too many different approaches to write them all down here, but the results are one of two things: a loop in the useEffect (which I somewhat understand, I think passing data to useEffect is causing it to set the parent's props and re-render the child every time because it is an object and isn't been deeply compared), or the following:
Cannot update a component (`App`) while rendering a different component (`PageComponent`).
The latter happens while attempting a few approaches without useEffect, such as:
if(data && data.lists && awaitingPageLayout) {
props.setLayout({ title: data.lists[0].title });
awaitingPageLayout = false;
}
I have also tried to use useQuery's onCompleted, which I believe resulted in another loop.
Hoping for a push in the right direction, I'm out of ideas here. Appreciate your time!
Related
I am currently stuck employing React's useEffect Hook. The project I am working on is quite complex, but I will try to break it down to the relevant parts in order to describe my issue.
A) The Child Component
import React, {useEffect, useState} from "react";
const ChildComponent = props => {
const [needsConfirmation, setNeedsConfirmation] = useState(true);
useEffect(() => {
setNeedsConfirmation(
!!props.someObject.someArrayOfObjects.find(({status}) => status === "added")
);
}, [props.someObject]);
// I SKIP THE RETURN PART
}
B) Inside The Parent Component
const ParentComponent = props => {
const renderChildComponent = someObject => {
return (
<ChildComponent
someObject={someObject}
/>
) : null;
};
return (
<>
props.someArrayOfObjectsHandledByRedux.map(renderChildComponent)
</>
)
}
C) The Problem
Inside the child component I would like the useEffect hook to take effect every time something about the property props.someObject changes. The latter one is an app-wide state handled by Redux. Of course, I made sure that the corresponding reducer function always returns a brand new object after an action has taken place.
The relevant code inside the reducer function looks something like this:
case ActionTypes.SOME_ACTION_SUCCESS:
return {
...state,
someObject: formattedResponse(data),
};
However, no matter what I did, I just cannot get the useEffect to take effect. Using the ReduxDevTools I could clearly verify that the reducer function has successfully returned a new state object. Nevertheless, the useEffect hook inside the child component did not recognize any changes in the dependencies.
For now, I have implemented a very ugly workaround by always creating a brand new object which is handed to the child component
const renderChildComponent = someObject => {
return (
<ChildComponent
someObject={{...someObject}}
/>
) : null;
};
But this is very inefficient as it automatically re-renders all child components, even though, typically, only the data of one particular child component might have changed.
Does anybody have an idea what the problem in this particular case might be?
Thanks a lot for your input in advance.
In the profiling result the component (Table) uses React.memo and it is shown that did not re-render
but underneath there is shown the same Table component and in Why did this render is mentioned that parent re-rendered
The Table uses memo this way and the querystring stays the same.
function areEqal(prevTable, nextTable) {
return prevTable.queryString === nextTable.queryString;
}
const MemoTable = memo(Table, areEqal);
...
<MemoTable queryString={queryString} />
The question is, why the Table is shown twice (once not re-rendered, then yes) and why it re-renders when memoized to re-render just when querystring changes.
memo can only prevent renders related to props. Rendering may still happen if a context value that you're subscribed to changes. As discussed in the comments, you are apparently using a couple of contexts, and those are what's causing the render.
While deleting the useContexts appears to have worked for your case, the fact that you weren't expecting this rerender may indidate that the DataContext.provider has a common mistake in which you're changing the context value on every render. If you're providing an object, you need to make sure to only create a new object when the properties of the object have actually changed. So for example, if your current code is this:
const Example = ({ children }) => {
const [userPreferences, setUserPreferences] = useState('something');
return (
<DataContext.Provider value={{ userPreferences, setUserPreferences }}>
{children}
</DataContext.Provider>
);
}
... then you should memoize the value like this:
const Example = ({ children }) => {
const [userPreferences, setUserPreferences] = useState('something');
const value = useMemo(() => {
return { userPreferences, setUserPreferences }
}, [userPreferences]);
return (
<DataContext.Provider value={value}>
{children}
</DataContext.Provider>
);
}
As for the two copies of Table in the dev tools, the first is the memoized component MemoTable. MemoTable then renders an unmemoized Table inside of it, and that's the second one. "Table (memo)" is the default name that memo gives to the component it returns. If you'd like to change that name so it shows up differently in the dev tools, you can:
const MemoTable = memo(Table, areEqal);
MemoTable.displayName = "Hello world";
I have a little problem with my code. When I render my component I've got this error on console console
Maximumupdate 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.
Below is part of my sample code:
MainComponent:
const saveData = (data) => {
setDataArray(prevState => [...prevState, data]);
}
return (
<Fragment>
{dataArray.map((element, index) => {
return (<MyComponent data={element} index={index} save={saveData}/>)
})}
</Fragment>
)
dataArray is an array of objects
MyComponent:
const MyComponent = (props) => {
const [data, setData] = useState(props.data);
useEffect(() => {
props.save(data);
}, [data]);
}
Is the problem in updating the state up to main component? Or I can't render my child component using map()?
Notice what happens when a child component updates its data:
It calls parent.saveData
This triggers a state change in the parent because the data array has changed
Because you didn't pass a key prop to children, React has no choice but to re-render all children with the updated data
Each child notices that its data prop has changed. This triggers the useEffect, so it also calls parent.saveData
The process repeats while the original render is still taking replace, etc, etc
Solution
Pass a key prop to each child. Do not use the index as key, instead use something that reflects the data being passed to the components
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.
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.