Trace why a React component is re-rendering
Asked Answered
F

8

442

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?

Federalist answered 6/12, 2016 at 20:50 Comment(4)
Maybe you could use shouldComponentUpdate to disable automatic component update and then start your trace from there. More information can be found here: facebook.github.io/react/docs/optimizing-performance.htmlRatel
@jpdelatorre 's answer is correct. In general, one of React's strengths is that you can easily trace data flow back up the chain by looking at the code. The React DevTools extension can help with that. Also, I have a list of useful tools for visualizing/tracking React component re-rendering as part of my Redux addons catalog, and a number of articles on [React performance monitoring](httOtolaryngology
Check github.com/welldone-software/why-did-you-renderCouncillor
I've tried this method and It's very good to locate rerenders triggered by hooks github.com/facebook/react/issues/16477#issuecomment-591546077Adonai
P
314

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"

React Dev Tools > Settings

Screenshot of React Devtools profiler

Policeman answered 23/12, 2020 at 14:14 Comment(9)
Firefox Link: addons.mozilla.org/en-US/firefox/addon/react-devtoolsSeltzer
For obvious reasons this should be the accepted answer as it lets you profile with ease and pinpoint the cause without creating ugly scripts within the codebase.Endoparasite
One thing that led me to use a script similar to stackoverflow.com/a/51082563 is the fact that the React DevTools don't show the differences between the 'old' and 'new' props when the reason for a render is "Props Changed". Many times that won't matter, but when the props are values, it can help to see what's causing the re-render. It's also great for seeing why hooks are running again.Discrepancy
this does not work as expected as it does not show renders because of parent component renders, and also it almost always does not show the reason for the re-rednerEmbolus
This also doesn't work if you want to detect the reason for a re-render in a test (I'm asserting that my useQuery hook is only invoked once, and the test is failing -- WHY?)Lemnos
The profiler recorder freezes if you try to use it to debug an infinite rerender loopAnachronism
How do we figure out which hook or which context changed? Just saying hook changed or context changed isn't very helpful.Santoyo
sick! but yeah, it seems to use hooks by index so still not sure how to figure out which hook is "hook 2" :)Nena
@Santoyo look into components tab of the react profiler and select the component. There you will see both the hook, it's index and state.Slime
T
496

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>;
}
Theressa answered 28/6, 2018 at 12:9 Comment(9)
@yarden.refaeli I see no reason to have an if block. Short and concise.Stilbite
Along with this, if you find a piece of state is being updated and it isn't obvious where or why, you can override the setState method (in a class component) with setState(...args) { super.setState(...args) } and then set a breakpoint in your debugger which you will then be able to trace back to the function setting the state.Monnet
How exactly do I use the hook function? Where exactly am I supposed to call useTraceUpdate after I've defined it as you wrote it?Telegenic
In a function component, you can use it like this function MyComponent(props) { useTraceUpdate(props); } and it will log whenever props changesTheressa
This componentDidUpdate snippet works perfectly in most cases, but I'm getting TypeError: Cannot convert undefined or null to object other times. It causes a critical render error. Has anyone else seen this in a class component?Ribwort
@DawsonB you probably don't have any state in that component, so this.state is undefined.Theressa
@JacobRask does this work for object equality? what if my prop key is an object, would it work then? prevProps[key] !== val ? Object.entries(this.props).forEach(([key, val]) => prevProps[key] !== val && console.log(Prop '${key}' changed) );Dedededen
I defined useTraceUpdate and I used it in my function component. But it does not detect that props changed. And component still renders twice (I put console.log("call!") inside of it and I get two printouts in browser's console. What else can I do?Schiedam
This only works for re-renders caused by changing props, not for changing state from other hooks (useState, useReducer, useContext, ...)Bisexual
P
314

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"

React Dev Tools > Settings

Screenshot of React Devtools profiler

Policeman answered 23/12, 2020 at 14:14 Comment(9)
Firefox Link: addons.mozilla.org/en-US/firefox/addon/react-devtoolsSeltzer
For obvious reasons this should be the accepted answer as it lets you profile with ease and pinpoint the cause without creating ugly scripts within the codebase.Endoparasite
One thing that led me to use a script similar to stackoverflow.com/a/51082563 is the fact that the React DevTools don't show the differences between the 'old' and 'new' props when the reason for a render is "Props Changed". Many times that won't matter, but when the props are values, it can help to see what's causing the re-render. It's also great for seeing why hooks are running again.Discrepancy
this does not work as expected as it does not show renders because of parent component renders, and also it almost always does not show the reason for the re-rednerEmbolus
This also doesn't work if you want to detect the reason for a re-render in a test (I'm asserting that my useQuery hook is only invoked once, and the test is failing -- WHY?)Lemnos
The profiler recorder freezes if you try to use it to debug an infinite rerender loopAnachronism
How do we figure out which hook or which context changed? Just saying hook changed or context changed isn't very helpful.Santoyo
sick! but yeah, it seems to use hooks by index so still not sure how to figure out which hook is "hook 2" :)Nena
@Santoyo look into components tab of the react profiler and select the component. There you will see both the hook, it's index and state.Slime
S
92

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.

Supersensual answered 6/12, 2016 at 21:16 Comment(8)
Nitpick: "stateless" just means any component that doesn't use state, whether it's defined with class syntax or functional syntax. Also, functional components always re-render. You need to either use shouldComponentUpdate, or extend React.PureComponent, to enforce only re-rendering on change.Otolaryngology
You're right about the stateless/functional component always re-renders. Will update my answer.Supersensual
can you clarify , when and why functional components always re-renders? I use quite a bit of functional components in my app.Federalist
So even if you use the functional way of creating your component e.g. const MyComponent = (props) => <h1>Hello {props.name}</h1>;(that's a stateless component). It will re-render whenever the parent component re-renders.Supersensual
I was actually also under the impression that functional way of declaring components uses PureComponent until @Otolaryngology pointed it out and I tested it. It does render everytime parent component renders...Supersensual
This is great answer for sure, but it do not answer the real question, - How to trace what triggered a re-render. Answer of Jacob R looks promising in giving the answer to real problem.Timpani
What also causes rerender is any changes in the context consumer when implemented via useContext-hook instead of <SomeContext.Consumer>... .Applicator
The hardest to debug I think was the Parent component rerender for it might get too wide to control over when it is rerendered. ReactJs does have a defensive render mechanism to cover some rare use casesKopp
T
12

@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.

Thibault answered 1/3, 2018 at 16:23 Comment(2)
It's now called UNSAFE_componentWillReceiveProps(nextProps) and it's deprecated. "This lifecycle was previously named componentWillReceiveProps. That name will continue to work until version 17." From the React documentation.Expansion
You can achieve the same with componentDidUpdate, which is arguably better anyway, since you're only wanting to find out what caused a component to actually update.Burbank
I
8

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.

Indore answered 16/3, 2019 at 10:4 Comment(2)
To save others some googling: npm deep-diff, deep-diff source at github. (The source link is the "repository" link on the npm page.)Mindamindanao
Best to use ref comparison when determining whether the prop value changed. React uses reference comparison for props, so if you had two different instances of ['a'], deep_diff would report them as unchanged, but React would say they changed. No problem using deep_diff for the logging output, though.Stogy
F
7

Thanks to https://mcmap.net/q/80518/-trace-why-a-react-component-is-re-rendering 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://mcmap.net/q/80518/-trace-why-a-react-component-is-re-rendering
 */
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.

Fugleman answered 12/3, 2021 at 18:3 Comment(2)
Works great. Would be nice if this was published as little npm package.Deity
Yeah, someday maybe if I find the time! :D Would probably use TSDX as starter.Fugleman
G
6

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);
Godard answered 24/2, 2020 at 10:41 Comment(0)
C
3

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 thereenter image description here

It will similar to above image along with the diff key.

Charis answered 13/7, 2018 at 7:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.