28

I have a parent component that renders a collection of children based on an array received via props.

import React from 'react';
import PropTypes from 'prop-types';
import shortid from 'shortid';
import { Content } from 'components-lib';
import Child from '../Child';

const Parent = props => {
  const { items } = props;

  return (
    <Content layout='vflex' padding='s'>
      {items.map(parameter => (
        <Child parameter={parameter} key={shortid.generate()} />
      ))}
    </Content>
  );
};

Parent.propTypes = {
  items: PropTypes.array
};

export default Parent;

Every time a new item is added, all children are re-rendered and I'm trying to avoid that, I don't want other children to be re-rendered I just want to render the last one that was added.

So I tried React.memo on the child where I'll probably compare by the code property or something. The problem is that the equality function never gets called.

import React from 'react';
import PropTypes from 'prop-types';
import { Content } from 'components-lib';

const areEqual = (prevProps, nextProps) => {
  console.log('passed here') // THIS IS NEVER LOGGED!!
}

const Child = props => {
  const { parameter } = props;
  return <Content>{parameter.code}</Content>;
};

Child.propTypes = {
  parameter: PropTypes.object
};

export default React.memo(Child, areEqual);

Any ideas why?

Joana Deluca Kleis
  • 295
  • 1
  • 3
  • 7
  • 2
    It won't log anything until props are changed. Try to tweak props in your parent. – kind user Nov 22 '19 at 14:35
  • you have an error in import ` import Child from '../Child' ` not sure if that is the cause. – Afia Nov 22 '19 at 14:52
  • 2
    If you are trying to prevent unnecessary rerenders of your children components, you should give each one a unique key. React has a very delicate way of dealing with keys, and if a key of a component changes, then React rerenders it completely. If you generate a new key everytime, React is going to rerender everything any time the props in the parent change. – Konstantin Nov 22 '19 at 14:55
  • 1
    @konstantin you are a genius!! I removed the key generation and it worked like a charm!! :D the equality fn is now being called and I could do the comparison. Thanks!! Can you add this as an answer so I can vote as the correct one? – Joana Deluca Kleis Nov 22 '19 at 15:07
  • Glad I could help :), will add it now – Konstantin Nov 22 '19 at 15:10

4 Answers4

21

In short, the reason of this behaviour is due to the way React works.

React expects a unique key for each of the components so it can keep track and know which is which. By using shortid.generate() a new value of the key is created, the reference to the component changes and React thinks that it is a completely new component, which needs rerendering.

In your case, on any change of props in the parent, React will renrender all of the children because the keys are going to be different for all of the children as compared to the previous render.

Please reference this wonderful answer to this topic

Hope this helps!

Konstantin
  • 1,390
  • 6
  • 18
6

I was having the same issue and the solution turned out to be just a novice mistake. Your child components have to be outside of the parent component. So instead of:

function App() {
  const [strVar, setStrVar] = useState("My state str");

  const MyChild = React.memo(() => {
    return (
      <Text>
        {strVar}
      </Text>
    )
  }, (prevProps, nextProps) => {
      console.log("Hello"); //Never called
  });

  return (
    <MyChild/>
  )
}

Do it like this:

const MyChild = React.memo(({strVar}) => {
  return (
    <Text>
      {strVar}
    </Text>
  )
}, (prevProps, nextProps) => {
   console.log("Hello");
});


function App() {
  const [strVar, setStrVar] = useState("My state str");

  return (
    <MyChild strVar = {strVar}/>
  )
}
2

Another possibility for unexpected renders when including an identifying key property on a child, and using React.memo (not related to this particular question but still, I think, useful to include here).

I think React will only do diffing on the children prop. Aside from this, the children prop is no different to any other property. So for this code, using myList instead of children will result in unexpected renders:

export default props => {
  return (
    <SomeComponent
     myLlist={
      props.something.map(
        item => (
          <SomeItem key={item.id}>
            {item.value}
          </SomeItem>
        )
      )
     }
    />
  )
}

// And then somewhere in the MyComponent source code:
...
{ myList } // Instead of { children }
...

Whereas this code (below), will not:

export default props => {
  return (
    <SomeComponent
     children={
      props.something.map(
        item => (
          <SomeItem key={item.id}>
            {item.value}
          </SomeItem>
        )
      )
     }
    />
  )
}

And that code is exactly the same as specifying the children prop on MyComponent implicitly (except that ES Lint doesn't complain):

export default props => {
  return (
    <SomeComponent>
    {props.something.map(
      item => (
        <SomeItem key={item.id}>
          {item.value}
        </SomeItem>
      )
    )}
    </SomeComponent>
  )
}
Zach Smith
  • 8,458
  • 13
  • 59
  • 133
0

I don't know the rest of your library but I did some changes and your code and (mostly) seems to work. So, maybe, it can help you to narrow down the cause.

https://codesandbox.io/s/cocky-sun-rid8o

Afia
  • 683
  • 5
  • 17
Lhew
  • 584
  • 9
  • 22
  • 1
    Thanks for taking your time and for creating that code sand box. I appreciate it! My case is a little different, you are explicity changing the property when you click the button and that triggers the fn. In my case, I'm just adding a new item to the parent's array, creating a new child every time and expecting the fn to be triggered. I just found out that the problem was the key that I was randomly generating. – Joana Deluca Kleis Nov 22 '19 at 15:09