Shared Element Transitions with React Hooks
Previously we worked on FLIP Animations, and how they help with smoother layout-change based animations. Shared Element Transitions basically takes that concept to the next level, and we’ll talk about how to do that with React Hooks.
Let’s recap what FLIP means.
First – Get the first position/size of the element
Update – Perform the action needed for the layout to update
Last – Get the last position/size of the element
Calculate – Calculate the positional and scale offset
Invert – Transform the element to make it appear at its initial position/scale
Paint – Browser paints the thing, so as of now nothing has visibly changed
Play – Trigger the animation from the transformed position/scale, to actual position/scale
In the last experiment, Update meant making the same element appear elsewhere, possibly in a different size on the page. It would be the same element, so it was a bit easier to animate since you can maintain a reference to that element and make transform-ed changes.
For “Shared Element Transitions”, it’s a bit different. Here’s what the idea is:
You have multiple screens/views/routes which share some contextual data. For example, one screen is a list of users, and the other is details for a given user. When you tap on a given user, their avatar animates to expand to the top of the screen, and the rest of the details are below it.
If that doesn’t make it clear, here’s an example picked from the Android Google Blog to make it clear as day:
So, you somehow need to animate the elements which contain the same kind of thing, but exist in two different screens as two completely separate elements. Achieving this isn’t that hard. All you need to do is change what Update does.
react-router for routing, but any routing thing works. We just need to change the view depending on some context. This is probably not super relevant here.
The only real challenge was, how to get the first and last element state reliably? Turns out, there’s a hook for that. The
useLayoutEffect hook is guaranteed to run after the layout computation is done, but nothing has been painted to the browser yet. Normally you would use the
useEffect hook, but there is one good use-case for using
useLayoutEffect, and it’s this one.
So how do we use it? Let’s go over it step-by-step.
- I created an HOC (
SharedElement) to pass an ID (say:
id-001) that will be assigned to the element, and to initialize the
useSharedElementTransitionhook. When this hook it initialized, it’ll store the First state of the element. But this all will end up counting for nothing, as our first meaningful action here will be to trigger a route change.
- The content in the new route will also have an element with the ID (
id-001) to indicate that both of these are basically the same thing. As the new route content is being initialized, it also has that element with ID
id-001wrapped in the
SharedElementHOC. That means this also has that hook initialized on it. This allows us to store the First state of the element with ID
id-001. Since this content has not been rendered yet, at the moment of initializing the hook, we get the
boundingClientRectof the element as it was present on the first route.
- When the layout computation is done but not painted to the screen, our
useLayoutEffectcomes into play. This logic will allow is to store the Last state of the element.
- Subsequently, we’ll calculate the position and scale deltas from the First and Last states, and use these values to define the first keyframe of the animation.
- And the rest is history. The second keyframe is still going to be
transform: none, the browser would have painted the screen when the animation would come into effect, and the element would appear to animate smoothly across screens.
That is it. Link to the codebase and demo are at the top.
Hooks have made it pretty easy to work about certain things, this is one of those things that I think looks much cleaner with hooks than it would with class components. Thanks, React team!