I think rendering lists of data it's a very common pattern for every application and therefore we as front end engineers have to deal with the most efficient way of displaying these lists, depending on the number of items, the kind of data we are displaying (images, numbers, text, inputs, etc.) and how the user will interact with these lists, by sorting them, deleting items, adding items, and all of those fun CRUD operations we're all familiar with.
Let's begin by defining the tech stack for this comparison:
- Core environment
- React 17
- using create-react-app + typescript -
- @stitches/react
- CSS-in-JS library with SSR (just for styling)
- react-use-hooks
- Just hooks for handling the list manipulation
- nanoid
- package to generate random unique ids, just for adding and removing the dummy elements
- Handling transitions
- react-transition-group
- "Exposes simple components useful for defining entering and exiting transitions" - package mantained by the react community
- animation libraries
- Anime JS
- Very popular animation library with a flexible api and multiple options
- Motion One JS
- lightweight and modern animation library made by the creator of framer-motion
- framer-motion
- Production-ready declarative animations.
- react-flip-move
- Animation module that handles list animation (excluseve
Of course there are tons more of alternatives (react-spring, gsap, popmotion, velocity js, but I won't be covering all in this posts, I will cover the ones I prefer and I have worked before to achieve nice animations
I have setup a repository and a deployment to vercel so you can check out the examples by yourself:
github: https://github.com/IrvingArmenta/animating-lists-react
example: https://animating-lists-react-cosmos.vercel.app/
experiment implementation with CRA and typescript
I want to make clear that we won't manage animations with CSS at all. my main reason is the usage of callbacks and the async control to have over animations. There are callbacks for CSS animation yes, but they're not as common and doing duration management because cumbersome, that being said, let's continue.
Animation Lists
Animejs + react-transition-group
First let's analyze the setup using animejs
and react-transition-group
Here it's a very simplified version for the anime js list source code AnimatedLists/lists/AnimeJsList.tsx
actual source code in github
import { Transition, TransitionGroup } from 'react-transition-group';
import anime from 'animejs';
const AnimeJsList = () => {
return (
<ul>
<TransitionGroup component={null}>
{items.map((item, index) => {
const liRef = createRef<HTMLLIElement>();
return (
<Transition
timeout={{ enter: duration.enter, exit: duration.exit }}
key={item.id}
nodeRef={liRef}
onEnter={() => {
if (liRef.current && parentUlRef.current) {
anime({
targets: liRef.current,
opacity: [0, 1],
height: [0, liRef.current.offsetHeight],
marginTop: [0, 8],
duration: duration.enter
});
}
}
}}
onExit={() => {
if (liRef.current) {
anime({
targets: liRef.current,
duration: duration.exit,
opacity: 0,
height: 0,
marginTop: 0,
translateY: 0
});
}
}}
>
<li ref={liRef} key={item.id}>
<NameHolder i={index} />
<p>key: {item.id}</p>
<button onClick={() => remove(item.id)}>
<DeleteIcon />
</button>
</li>
</Transition>
);
})}
</TransitionGroup>
<ul/>
);
};
As you can see, we are using <TransitionGroup />
and <Transition />
to wrap the {items.map()}
array. Here we are listening for the onEnter
and the onExit
callback of the <Transition />
component.
Before the usage of hooks <Transition />
used to get the child node elements through the findDomNode
api, which is now deprecated in strict mode, so if you try to use it as before, it will throw a warning/error, therefore, you need to setup the usage by setting the nodeRef={}
prop where the ref
will be a a reference to the HTML node, in this case the <li />
inside the list.
const liRef = createRef<HTMLLIElement>();
...
<Transition nodeRef={liRef} />
...
<>
<li ref={liRef} key={item.id}>
<NameHolder i={index} />
<p>key: {item.id}</p>
<button onClick={() => remove(item.id)}>
<DeleteIcon />
</button>
</li>
</>
...
In order for the reference to work for each <li />
item, we need to use createRef
inside the array map, since we are creating and removing elements createRef
make these refs dynamically for us.
on both onEnter
and onExit
callbacks we are calling the anime()
function to apply the JS animation:
anime({
targets: liRef.current,
opacity: [0, 1],
height: [0, liRef.current.offsetHeight],
marginTop: [0, 8],
duration: duration.enter
});
targets
it's the reference to the DOM of the <li />
element, but we could target other elements inside the list, or other multiple selectors, like #id
or .class
selectors.
We set all of the props to start with [0]
and then animate to the end values, setting an array in the values, this it's equivalent to set the style immediately, soopacity
will start on 0
and animate to 1
, height will animate from 0
to the actual <li />
offsetHeight, this is a way to achieve animation to 'auto' without setting a fixed height.
All of these values will be animated at the same duration (800ms in the example) and the default easing
by animejs
animejs default easing it's a weird "elastic" easing, so sometimes it's hard to understand the animation timing, therefore I would recommend setting it to
linear
and try other easings from there.
https://animejs.com/documentation/#linearEasing
the animation function has multiple callbacks that can help us orchestrate other side effects, such as complete
, update
and begin
. these are extremely helpful when we want to handle events after an animation has concluded or when it has reached a certain point.
I won't be going super deep in explaining the functionality of each approach, so please if you're interested in animejs
read their documentation: https://animejs.com/documentation
Let's review this approach:
-- bundle size --
bundlephobia - animejs - Minified and gzipped - 6.9kb
buundlephobia - react-transition-group - Minified and gzipped - 4.4kb
Total bundle size: 6.9kb
+ 4.4kb
= 11.3kb
My opinion
I think this approach for adding animation into your application gives you the flexibility to not just animate lists, but also other so many details, however, it lacks a lot of modern features, like dragging, sorting, and layout based animations, these have to be implemented by yourself, if you don't need such features, this is a very solid way to build some animation into your components!
it also has a very well explained documentation and support from a big community.
Motion JS + react-transition-group
This approach is very similar to the one above, so I won't be explaining a lot of it, there are some key differences though.
Here it's the simplfied version, just like the animejs
one, file name is: AnimatedLists/lists/MotionJsList.tsx
- actual source code
import { animate } from 'motion';
import { Transition, TransitionGroup } from 'react-transition-group';
const MotionJsList = () => {
return (
<ul>
<TransitionGroup component={null}>
{items.map((item, index) => {
const liRef = createRef<HTMLLIElement>();
return (
<Transition
timeout={{ enter: duration.enter, exit: duration.exit }}
key={item.id}
nodeRef={liRef}
onEntering={() => {
if (liRef.current && parentUlRef.current) {
animate(
liRef.current,
{
opacity: [0, 1],
height: [0, `${liRef.current.offsetHeight}px`],
marginTop: [0, `${8}px`]
},
{ duration: duration.enter / 1000 }
);
}
}
}}
onExit={() => {
if (liRef.current) {
animate(
liRef.current,
{
opacity: 0,
height: 0,
marginTop: 0
},
{ duration: duration.exit / 1000 }
);
}
}}
>
<li ref={liRef} key={item.id}>
<NameHolder i={index} />
<p>key: {item.id}</p>
<button onClick={() => remove(item.id)}>
<DeleteIcon />
</button>
</li>
</Transition>
);
})}
</TransitionGroup>
</ul>
);
};
As you may notice already, the setup is quite similar to theanimejs
one, however, the function that applies the animation for the node reference has a different name (anime => animate
) and different syntax:
animate(
liRef.current,
{
opacity: [0, 1],
height: [0, `${liRef.current.offsetHeight}px`],
marginTop: [0, `${8}px`]
},
{ duration: duration.enter / 1000 }
);
animate
takes the node reference as the first attribute, then followed by the actual animation values, these are quite similar to animejs
in the sense that we are also setting [0]
at the beggining to specify that we want to start from that point to the next at the end of the array.
the animate
function do not take number
(unless is 0
) as a valid type to animate to, it needs a unit string
, just like we are doing here with the offsetHeight, ${liRef.current.offsetHeight}px
, if we don't do this, it won't know what value to apply, but it can be anything from px
to rem
to em
etc.
Another thing to point at, it's that the duration
value is set in a different object attribute, after the animation values, (animejs
just put all of those in one single object), also, duration
is measured in seconds and not milliseconds like animejs
, therefore we have to divide the 800ms
by 1000
in order to get the 0.8s
value.
Motion One
was made. by the actual creator of framer-motion
, as a more web browser native experience, appealing to the (always changing) WAAPI (web animations api) it applies animations in the most performant and efficient way, using the GPU
layers to not overload the main JS thread, and providing the most modern way of interacting with the WAAPI.
Animations with motion one do not depend in the main JS thread, you can see their example in their website where you can block Javascript and it won't interrupt the 60fps animation,
more information here: https://motion.dev/guides/feature-comparison#hardware-acceleration
Let's review this approach:
-- bundle size --
bundlephobia - motion - Minified and gzipped - 5.5kb
buundlephobia - react-transition-group - Minified and gzipped - 4.4kb
Total bundle size: 5.5kb
+ 4.4kb
= 9.9kb
( 1.4kb
smaller than animejs
)
My opninion
If you're concerned about bundle size, I think this is the best approach in 2021, however, there is not much support from legacy browsers, like IE11 and the features are also lacking in comparison to animejs or gsap, therefore I'm not sure if it's ready for production.
I would keep an eye for the improvement and any future releases, since I think it's becoming the most performant and updated package out there.
Framer motion
framer motion it's a whole new different approach since it does not depend on react-transition-group
to handle the animations, it has it's own way for watching exits and enterings of elements in a list, and we will see a simple setup for lists in the next code example:
This example uses
framer motion 4
sincecreate-react-app
has no support foresm
packages and there are some conflicts, inframer motion 5
theAnimateSharedLayout
component is removed and any component with thelayout
prop works smartly without the wrap, more information: https://www.framer.com/docs/guide-upgrade/##esm-and-create-react-app
I will paste here the simplified version of the source code, but you can go to the actual source code - AnimatedLists/lists/FramerMotionList.tsx
import { AnimatePresence, AnimateSharedLayout, motion } from 'framer-motion';
const FramerMotionList = () => {
return (
<ul>
<AnimateSharedLayout>
<AnimatePresence initial={false}>
{items.map((item, index) => {
return (
<motion.li
key={item.id}
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { default: duration.enter / 1000 }
}}
exit={{
opacity: 0,
transition: { default: duration.exit / 1000 }
}}
layout={true}
onAnimationStart={() => onFramerAnimationStart(item.id)}
>
<NameHolder i={index} />
<p>key: {item.id}</p>
<button onClick={() => remove(item.id)}>
<DeleteIcon />
</button>
</motion.li>
);
})}
</AnimatePresence>
</AnimateSharedLayout>
<ul/>
);
};
framer motion has it's own component to check for "presence", this handles the exit
and animate
states of the elements, it's called <AnimatePresence />
framer motion animations need the motion
helper to use as DOM references, this is how framer motion
it's aware of the presence of these items, therefore the simple <li />
became <motion.li />
but you can render any DOM HTMLElement, such as <motion.div />
or <motion.ul />
etc.
animate
works as the onEnter
callback for new elements and exit
works as the onExit
callback for removed elements in the list.
As you can see here, we are just defining an opacity animation, nothing else, and a duration of 0.8s
just like motion js
,
<motion.li
key={item.id}
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { default: duration.enter / 1000 }
}}
exit={{
opacity: 0,
transition: { default: duration.exit / 1000 }
}}
layout={true}
onAnimationStart={() => onFramerAnimationStart(item.id)}
>
Instead of passing array values as previous examples ([0, 8]
) we are setting the initial={{ opacity: 0 }}
value from where the animation will happen when a new <motion.li />
it's added, animate={}
will animate it to opacity: 1
and exit={}
will animate to 0
when removed.
Notice the layout={true}
prop, in this case, we are listening for the changes in all the motion.li
elements, in order for the list to smoothly re-arrange itself, if layout
is set to false
the elements will abruptly change, this is why, in this example, we don't need to set a height
animation for a smooth transition, pretty convenient!
<motion />
elements have listeners for animation end and animation start, there is also an specific listener for the layout change animation, for more information: https://www.framer.com/docs/component/##animation-events
Let's review this approach:
-- bundle size --
bundlephobia - framer motion - Minified and gzipped - 41.9kb
Total bundle size: 41.9kb
- [32kb
more than motion js
and react-transition-group
]
the face of bundle size phobia...
Bundle size difference is huge however, there are multiple alternatives to reduce the impact of the package, you can learn more here: https://www.framer.com/docs/guide-reduce-bundle-size/
the bundle size could be reduced to even 13kb
, however, it depends on the features you want to use, like drag
and dom interactions
My Opinion
As the package says itself, framer-motion
it's a production ready animation library, it comes with a bundle of really nice features, a lot of seems like magic, and that's when some of the issues come up, there is a lot of "magic" going on that sometimes customizing to your needs, can become specially difficult.
In the other hand, it's extremely helpful and I woulnd't mind it using it in my next react project (I would use the lazy loading though)
React flip move
react-flip-move
it's an old (2016!) but still functional package (until certain degree...)
In comparison with the other approaches, react-flip-move
it's a library that has the main purpouse in animating lists with the Flip
(First, Last, Invert, Play.) technique, the animation runs smoothly and the layout adapts itself.
Let's check the source code (remember, simplified source code!):
here it's the actual code AnimatedLists/lists/FlipMoveList.tsx
import FlipMove from 'react-flip-move';
const FlipMoveList = () => {
return (
<ul>
<FlipMove typeName={null} onStart={(el) => onFlipMoveStart(el)}>
{items.map((item, index) => {
return (
<li key={item.id}>
<NameHolder i={index} />
<p>key: {item.id}</p>
<button onClick={() => remove(item.id)}>
<DeleteIcon />
</button>
</li>
);
})}
</FlipMove>
</ul>
);
};
Here you can see that the code for the FlipMove
component it's the "simplest" one of all previoous examples, this is because FlipMove
still uses the old findDomNode
api, therefore, we will get the deprecation warning/error in strict mode.
FlipMove
also uses an old API, componentWillReceiveProps
which has also already being deprectated and marked as UNSAFE_
, so I don't think FlipMove
it's available for production anymore, unless it gets mantained.
unlike the other libraries, react-flip-move
does not have exactly defined styles for enter
or exit
, it uses various pre-defined animations and values for element transitions. You can set the next values:
- easing
- duration
- delay
- staggerDurationBy
- staggerDelayBy
These gives you less specific control for the elements but the presets and the "experiment" here can give you an idea of how these values affect the animations : Lab
<FlipMove typeName={null} onStart={(el) => onFlipMoveStart(el)}>
In the setup for <FlipMove />
we have set the typeName={null}
to null in order to not wrap the elements in any other HTML element. by default <FlipMove />
renders a <div />
that wraps the children.
It has multiple event listeners, one example is onStart()
, however you need more implementation to get the entering
element reference correctly, since it seems this event listens each element in the list.
react-flip-move
and framer motion
both support the animation of re-ordering the list out-of-the-box, no need for any kind of extra implementation. animejs
and motion
needs some way to understand the position of the elements and from there animate and properly move. Here it's an article that may lead to that implementation, however I don't cover that in this blogpost -> https://itnext.io/animating-list-reordering-with-react-hooks-aca5e7eeafba
let's review this final approach:
-- bundle size --
bundlephobia - react-flip-move - Minified and gzipped - 5.5kb
buundlephobia - react-transition-group - Minified and gzipped - 4.8kb
Total bundle size: 4.8kb
[ the smallest alternative for lists animation in this post ]
My Opinion
I think react-flip-move
it's a great library for animating lists, the re-ordering feature comes included and animations are quite performant, however, at 2021, it has become stale and deprecation notices are flowing, I'm not sure it will be available for React 18
. In conclusion I wouldn't recommend it for future projects and would look for more a modern implementation if possible, it seems the creator encourage the usage of another library -> react-flip-toolkit
Conclusion
As I said in the beginning, displaying lists of data is a common pattern in the front end work, and animation make these data to be more presentable and attractive to users.
I always recommend implementing these kind of animations, it really changes the feeling for the end users when interacting with data, and I think it's also part of the idea of micro-interactions
which I really love, perhaps I should write a blog post about it.
I'll write a table with some points and features (good and bad) for each approach
RTG = react-transition-group
library/feature | Anime JS + RTG | MotionJS + RTG | Framer motion | react flip move |
---|---|---|---|---|
Bundle size | 11.3kb | 9.9kb | 41.9kb | 4.8kb |
SSR (no style flashing) | ![]() |
![]() |
![]() |
![]() |
Flexibility (usage besides lists) | ![]() |
![]() |
![]() |
![]() |
Good documentation | ![]() |
![]() (some docs are not exposed ) |
![]() |
![]() |
Simple setup | ![]() |
![]() |
![]() |
![]() |
Best GPU handling | ![]() |
![]() |
![]() |
![]() |
List re-ordering animation | ![]() |
![]() |
![]() |
![]() |
For more specifics in motion
differences, they have this page right here:
https://motion.dev/guides/feature-comparison.
Thank you very much for your time and the interest in this post, let me know if you have any interest in animation an which approach it's the better one for you? (of course I think depends on the project and needs!)
Here are again, both github links and the deployment in vercel for this simple post!
github: https://github.com/IrvingArmenta/animating-lists-react
example: https://animating-lists-react-cosmos.vercel.app/
Irving.