Animating list reordering with React Hooks

FT Product & Technology
Aug 26 · 7 min read

By Tara Ojo

My team recently worked on a feature to give users shortcuts to topics they follow in the FT’s App after we trialled it as an AB test and got successful results.

When we got new data from the API we wanted to have a sleek animation from the old order of topics, into the new refreshed order. We started experimenting with the feasibility of a few different animations, including one where each bubble would smoothly slide into its new position when we got its new order from the API — influenced by the Instagram story styled bubble component.

Animation of image bubbles re-ordering themselves
Animation of image bubbles re-ordering themselves

While it can be straightforward to do a whole load of animations and transitions with CSS, it took me a while to find an example of animating the reordering of list items, especially with React. Since I’ve also started to get used to the concepts of React Hooks I wanted to use them to implement this animation too.

I found this difficult to do using React hooks because my component would automatically rerender, in its new order, when it got new data. I was trying to hook into the moment before rerendering to smoothly transition from one state to another. Without the componentWillReceiveProps function call from the class components, this was hard to do.

I was under the (incorrect) assumption that there would be loads of React hooks examples out in the wild. I honestly just wanted a copypasta solution that I wouldn’t have to tweak too much 👀. I also didn’t want to bring in some huge, usually overly flexible package to reorder one small thing. I did come across a great post by Joshua Comeau (linked below). It explains how to do exactly what I needed, but with class components. With React hooks I needed to re-think some of the concepts to get it to work, but I’ve based the majority of this work on that post.

What we want to happen:

  1. Keep an eye out for when our element list is going to change
  2. When it changes we want to calculate the previous positions and the new positions of each element in the list before the DOM updates
  3. Also before the DOM updates with the new order of the list we want to “pause” the update and show a smooth transition of each item in the list from its old position to its new position
Two bubbles with arrows showing that they will swap position
Two bubbles with arrows showing that they will swap position

Let’s start with a parent component that just renders the children that is passed into it, AnimateBubbles:

Then we can use that component by rendering our items inside of it. In my case I’ve created a Bubble component that adds the styles to make each image a circle, the full code is here. The Bubble component also forwards the ref onto the DOM element. This is important as we can use the ref to find where the element is rendered in the DOM, then we can calculate its position. Another important prop is the key, this is not only needed for React when mapping over elements, but we can also use later to uniquely identify each item and match its old and new positions in the DOM.

Now that we have the foundations of our components we can start building out the logic of our AnimateBubbles component.

Keeping an eye out for React rerenders

Measure each position in the the DOM

In this function we pass in children as an argument and use the forEach function on React.Children to iterate over them, getting the measurements of each item in an object that we can later store in state. This is where setting a key on each child is important as we store each box with its key as the object key so we can match up the old position with the new position later. It is also why creating a ref for each child is important, as we use that to find the element in the DOM and measure the bounding box for it. Now, when we call this function inside of the useEffect we will get the bounding box for each child updated every rerender 🎉.

The problem now is we’re only getting the new positions, but we also need the old positions so we can do the slide animation from old position to new position.

Getting the previous state/props with the usePrevious hook

I’ve put this into a separate useEffect as they don’t need to be done together and they both have different dependencies. These previous positions will now be recalculated every time children change.

Now we have these two important pieces of information, we can move on to the actual transition 😅

Making moves

So in our case, we find the first position of each child. We have this stored in state in prevBoundingBoxes. Then we find the last position of each child. We also have this stored in state in boundingBoxes. The next step is to invert, which is to figure out how each child has changed and apply those transformations to each child so it looks like it is in its first position.

We can set up a new useEffect to do this with dependencies on children, boundingBoxes and prevBoundingBoxes as we’ll use all of those values in the effect. Remember when React rerenders, it instantly updates the view with the new state, but we can use requestAnimationFrame to tell the browser that we want to perform an animation. The browser will call the function you give it before the next repaint.

This we can do with first — last = inverted-value on the left values of the box. Since we also have a reference to the DOM node, we can apply the transform straight onto the node with a transition of 0 seconds so it inverts instantly.

The final step is to play the animation. To do this we wait for the child elements to be inverted, then we remove the transform and apply a smooth transition. The elements then slide into their new positions 💃🏾💃🏾💃🏾.

Remove the glitch

“Unlike componentDidMount or componentDidUpdate, effects scheduled with useEffect don’t block the browser from updating the screen. This makes your app feel more responsive. The majority of effects don’t need to happen synchronously. In the uncommon cases where they do (such as measuring the layout), there is a separate useLayoutEffect Hook with an API identical to useEffect.” — React docs

Since our aim is to measure the layout of elements in the DOM, what we actually need is useLayoutEffect. As noted, useLayoutEffect has an identical API to useEffect so I could easily switch out one for the other and it made the whole animation look super smooth 😎

Going Live?

Extras

FT Product & Technology

A blog by the Financial Times Product & Technology…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store