Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected behaviour : function vs functional component and animations

I display a list of foos and when i click on some link more results i keep the existing foos and i append to them the new ones from my api like bellow

const [foos, setFoos] = useState([]);
...
// api call with axios
...
success: (data) => {
    setFoos([ ...foos, ...data ])
},

Each <Foo /> component run the animation above

App.js

...
<div className="foos-results">
    { foos.map((foo, index) => <Foo {...{ foo, index }} key={foo.id}/>) }
</div>
...

Foo.js

const Foo = ({ foo, index }) => <div className="circle">...</div>

animation.css

.circle {
   ...
   animation: progress .5s ease-out forwards;
}

The problem is when i append the new ones then the animation is triggered for all the lines of <Foo />.

The behavior expected is that the animation is triggered just for the new ones and not starting over with the existing ones too.

UPDATE

We have found the origin of the problem (it's not related to the uniqueness of key={foo.id})

if we change

const Foo = ({ foo, index }) => <div className="circle">...</div>

to

const renderFoo = ({ foo, index }) => <div className="circle">...</div>

And App.js to

...
<div className="foos-results">
    { foos.map((foo, index) => renderFoo({ foo, index })) }
</div>
...

It works

So why is this behavior like this in react ?

here is a sandbox based on @Jackyef code

like image 694
Hayi Avatar asked Sep 18 '26 01:09

Hayi


1 Answers

This is quite an interesting one.

Let's look at the sandbox provided in the question.

Inside App, we can see this.

  const renderItems = () => (
    <div>
      {items.map((item, index) => (
        <div className="item" key={item.id}>
          <span>
            {index + 1}. {item.value}
          </span>
        </div>
      ))}
    </div>
  );

  const Items = () => renderItems();

  return (
    <div className="App">
      <h1>List of items</h1>
      <button onClick={addItem}>Add new item</button>
      <Items />
    </div>
  );

Seems pretty harmless right? The problem with this is that Items is declared in the App render function. This means that on each render, Items actually is now a different function, even though what it does is the same.

<Items /> is transpiled into React.createElement, and when diffing, React takes into account each components' referential equality to decide whether or not it is the same component as previous render. If it's not the same, React will think it's a different component, and if it's different, it will just create and mount a new component. This is why you are seeing the animation being played again.

If you declare Items component outside of App like this:

const Items = ({ items }) => (
  <div>
    {items.map((item, index) => (
      <div className="item" key={item.id}>
        <span>
          {index + 1}. {item.value}
        </span>
      </div>
    ))}
  </div>
);

function App() { /* App render function */}

You will see everything works as expected. Sandbox here

So, to summarise:

  1. Referential equality matters to React when diffing
  2. Components (function or class that returns JSX) should be stable. If they change between renders, React will have a hard time due to point number 1.
like image 143
Jackyef Avatar answered Sep 20 '26 15:09

Jackyef



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!