(V) Dismissible List
What we're going to be building
Why Though?
We just built a cool dismissible list in the previous recipe, and surely you're thinking,
why Gaurav? why are we rebuilding our component again? Well, we're not, open your web-inspector,
and take a closer look at the dom tree, doesn't it seem strange, that our list, which we dismissed,
still exists in the tree?
Now we're getting closer to the issue, as you see, useSpring
is very powerful while building UI centric animations,
and handling static data. Sure, with some smart logic, you can easily make it handle dynamic data as well, but according
to the react-spring docs we have a better API hook provided by the library to help us elegantly handle dynamic
and complex lists of data.
Hero of this Recipe
The useTransition
api hook, is designed to elegantly handle enter and exit transitions for components based on dynamic list of data.
Essentially, react-spring lets us avoid choppy behaviour, while mounting and unmounting our components based on data.
NOTE: We will use some hacky solutions to make useTransition
work according to our specifications.
Code (Part 1)
type Props = {
onDismiss: () => void;
} & PropsWithChildren;
const Dismissible = (props: Props) => {
const [spring, api] = useSpring(() => ({
from: {
x: 0,
height: 80,
scale: 0,
},
config: config.stiff,
}));
const bind = useDrag(
({ down, movement: [mx], velocity: [velocity], direction: [x] }) => {
let flingIt = false;
if (!down && velocity > 0.5 && x === 1) {
flingIt = true;
}
api.start(() => {
if (flingIt) {
return {
x: 400,
height: 300,
scale: 300,
onResolve: props.onDismiss,
};
} else if (spring.x.get() >= 0) {
return {
x: down ? mx : 0,
height: down ? mx : 80,
scale: down ? mx : 0,
};
} else if (!down) {
return {
x: 0,
height: 80,
scale: 0,
};
}
});
}
);
const height = spring.height.to({
map: Math.abs,
range: [160, 280],
output: [80, 0],
extrapolate: "clamp",
});
const scale = spring.scale.to({
map: Math.abs,
range: [0, 280],
output: [0, 1],
extrapolate: "clamp",
});
const commonProps = {
borderRadius: 10,
touchAction: "none",
};
return (
<animated.div
{...bind()}
style={{
x: spring.x,
height,
width: 160,
backgroundColor: "#ff6d6d",
position: "relative",
...commonProps,
marginBottom: "1rem",
}}
>
<animated.div
style={{
height,
width: 80,
scale,
backgroundColor: "#ff6",
position: "absolute",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
...commonProps,
}}
>
<animated.div
style={{
scale,
fontSize: "2rem",
color: "black",
}}
>
{props.children}
</animated.div>
</animated.div>
</animated.div>
);
};
export default Dismissible;
Code Breakdown
Finding the similarities:
This code, looks quite similar to the Dismissible, we just built. So then, what's changed? If you take a look at the code, you will see, we're using
useSpring
instead ofuseSprings
, since now, our goal, is to make a reusable, atomic dismissible, and we're remdering a single component, instead of a component array.Logic Required for Reusability
Since the only goal of our component is to be dismissed, we add a
onDismiss
callback handler to the props. We will discuss when and where to fire this callback down below.type Props = {
onDismiss: () => void; // <--- this
} & PropsWithChildren;Handling the Dismiss Action UI state
In Recipe 4 we wanted to track state of dismiss action triggered across the component, and thus we used
useState
to keep track of the same. In the current case though, we simply need to know about dismiss action triggered , over the course of the callback function insidedrag
handle. We've used a simple flag, aptly namedflingIt
. That is set totrue
, if the user flings the dismissible.let flingIt = false; // <--- this
if (!down && velocity > 0.5 && x === 1) {
flingIt = true;
}Handling the Dismiss Action Callback
The other major change, that might not be very visible to the eye, is when we've fired our
onDismiss
callback. This last but not the least, change is very important in explaining how react-spring actually handles animations and why it is said to be an optimised way to handle animations.
As you can see, we're firing ouronDismiss
callback,onResolve
of the animation. This is because, the animation controllers are async in nature, and do not block the main code execution. Sinceasync
functions are Promises, they need to berejected
orresolved
, in order for the main thread to take notice.if (flingIt) {
return {
x: 400,
height: 300,
scale: 300,
onResolve: props.onDismiss, // <--- this
};
}
Conclusion (Part 1)
That about explains the changes we have made, to the Dismissible
to make it more reusable.
There are a few other changes done as well, but you should be able to easily grasp them, since we have
covered all of the same in previous recipes. You can find the list of the same here.
Lets now move onto the useTransition
hook for the actual list rendering.
Code (Part 2)
type Props = {
initialList: string[];
};
const DismissibleList = (props: Props) => {
const [list, setList] = useState<string[]>([]);
const transitions = useTransition(list, {
from: { maxHeight: 0 },
enter: { maxHeight: 80 },
leave: { maxHeight: 0 },
trail: 200 / list.length,
config: config.stiff,
});
useEffect(() => {
setList(props.initialList);
}, [props.initialList]);
return transitions((styles, item) => (
<animated.div
style={{
...styles,
marginBottom: styles.maxHeight.to({
map: Math.abs,
range: [0, 80],
output: [0, 10],
extrapolate: "clamp",
}),
}}
key={item}
>
<Dismissible
onDismiss={() => {
setList((list) => list.filter((itm) => itm !== item));
}}
>
{item}
</Dismissible>
</animated.div>
));
};
export default DismissibleList;
Code Breakdown
Definition Enter/Exit Transitions and Component State
As you can see in the code-block below, we're defining an internal state for a list, for
useTransition
hook to use. You might ask, why is this important? The answer lies, in how useTransition tracks list data, since we want to be able to replace and reuse our component, more than once, we need to showuseTransition
, that our list at some point will be empty, and only then, new data will be accepted by theuseTransition
hook. (This is part 1 of our hack for reusable list)We're also defining parameters for the
useTransition
hook:- We've seen
from
be used before in previous recipes, this parameter is simply used to define the initial state of the list-item enter
isuseTransition
's equivalent toto
inuseSpring
, it is the state, onto which the list-item will transition to.leave
is a special parameteruseTransition
uses to unmount a list-item from the DOM tree with a transition.trail
is also a special parameter, which allows delay to be introduced, during the mounting of list-item.- We've seen
config
be used before, it allows our defined transitions to use spring physics.
const [list, setList] = useState<string[]>([]);
const transitions = useTransition(list, {
from: { maxHeight: 0 },
enter: { maxHeight: 80 },
leave: { maxHeight: 0 },
trail: 200 / list.length,
config: config.stiff,
});
useEffect(() => {
setList(props.initialList);
}, [props.initialList]);- We've seen
Component Rendering
The component render, is pretty similar to how
useSprings
wants components to be rendered, except thatuseTransition
actually wants us to render the same component as a list, thus it takes creates a function api for us to use, which takes our list-item JSX as function parameter.return transitions((styles, item) => (
<animated.div
style={{
...styles,
marginBottom: styles.maxHeight.to({
map: Math.abs,
range: [0, 80],
output: [0, 10],
extrapolate: "clamp",
}),
}}
key={item}
>
<Dismissible
onDismiss={() => {
setList((list) => list.filter((itm) => itm !== item));
}}
>
{item}
</Dismissible>
</animated.div>
));
Conclusion (Part 2)
Phew! That was a lot of information! I hope you found it useful, this is us barely scratching the surface with the potential of react-spring
as
a animation library for React
. There's a lot more you can do, but most of the components you will build, will end up using this core logic, thus make
sure to use this as a reference while building your projects! Cheers!