Skip to main content

(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

  1. 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 of useSprings, since now, our goal, is to make a reusable, atomic dismissible, and we're remdering a single component, instead of a component array.

  2. 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;
  3. 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 inside drag handle. We've used a simple flag, aptly named flingIt. That is set to true, if the user flings the dismissible.

    let flingIt = false; // <--- this
    if (!down && velocity > 0.5 && x === 1) {
    flingIt = true;
    }
  4. 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 our onDismiss callback, onResolve of the animation. This is because, the animation controllers are async in nature, and do not block the main code execution. Since async functions are Promises, they need to be rejected or resolved, 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

  1. 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 show useTransition, that our list at some point will be empty, and only then, new data will be accepted by the useTransition hook. (This is part 1 of our hack for reusable list)

    We're also defining parameters for the useTransition hook:

    1. We've seen from be used before in previous recipes, this parameter is simply used to define the initial state of the list-item
    2. enter is useTransition's equivalent to to in useSpring, it is the state, onto which the list-item will transition to.
    3. leave is a special parameter useTransition uses to unmount a list-item from the DOM tree with a transition.
    4. trail is also a special parameter, which allows delay to be introduced, during the mounting of list-item.
    5. 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]);
  2. Component Rendering

    The component render, is pretty similar to how useSprings wants components to be rendered, except that useTransition 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!