Skip to main content

(IV) Dismissible

What we're going to be building

(hint: Dismissibles!)

Code

const Dismissible = () => {
const [springs, api] = useSprings(2, () => ({
x: 0,
height: 80,
scale: 0,
config: config.stiff,
}));

const [flung] = useState(() => new Set<number>());

const bind = useDrag(
({
args: [index],
down,
movement: [mx],
velocity: [velocity],
direction: [x],
}) => {
if (!down && velocity > 1 && x === 1) flung.add(index);
api.start((i) => {
if (flung.has(i)) {
return {
x: 400,
height: 300,
scale: 300,
};
}
if (!flung.has(i) && i === index && springs[i].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,
};
}
});
if (flung.size === 2 && !down) {
setTimeout(() => {
api.start((i) => ({
height: 80,
x: 0,
scale: 0,
delay: i === 1 ? 500 : 0,
}));
flung.clear();
}, 600);
}
}
);

return (
<>
{springs.map((props, index) => {
const height = props.height.to({
map: Math.abs,
range: [160, 280],
output: [80, 0],
extrapolate: "clamp",
});

const commonProps = {
borderRadius: 10,
touchAction: "none",
marginBottom: 10,
};

return (
<animated.div
key={index}
{...bind(index)}
style={{
x: props.x,
height,
width: 160,
backgroundColor: index ? "#ff6" : "#ff6d6d",
position: "relative",
...commonProps,
}}
>
<animated.div
style={{
height,
width: 80,
scale: props.scale.to({
map: Math.abs,
range: [0, 280],
output: [0, 1],
extrapolate: "clamp",
}),
backgroundColor: index ? "#ff6d6d" : "#ff6",
position: "absolute",
...commonProps,
}}
/>
</animated.div>
);
})}
</>
);
};

Code Breakdown

I know your first thoughts will be, "Woah! that's a lot of code", you're not wrong, but we're gonna go over it step by step. We're gonna use react-spring with @use-gesture/react (a gesture recognition library by the amazing developer who created react-spring).

  1. Animation definition

const [springs, api] = useSprings(2, () => ({
x: 0,
height: 80,
scale: 0,
config: config.stiff,
}));

You will notice, we've used useSprings instead of useSpring, since this better suits our use-case. usSprings is meant to handle an array of components that have the same animation configuration. In our case, we will be using 2 array items for our dismissibles.

This time, we're using same configuration to animate multiple properties of components, such as

  • x
  • height
  • scale

These properties will be applied to the components same as we have been doing in recipe 1 and recipe 2.

But, You might have noticed, our list items have a springy effect on them when we release them after sliding, This is because of a property, config, config controls how the component value changes behave, similar to bezier-curve, except relying on spring physics instead. In our case we have assigned assigned config a value of config.stiff. This is because react-spring is meant to mimic (you guessed it) springs.
NOTE: The config object is an animation config preset object provided and imported from react-spring library.
Learn more about spring config here.

  1. Animation control (Part 1)

const [flung] = useState(() => new Set<number>());

const bind = useDrag(
({
args: [index],
down,
movement: [mx],
velocity: [velocity],
direction: [x],
}) => {
if (!down && velocity > 1 && x === 1) flung.add(index);
api.start((i) => {
if (flung.has(i)) {
return {
x: 400,
height: 300,
scale: 300,
};
}
if (!flung.has(i) && i === index && springs[i].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,
};
}
});
if (flung.size === 2 && !down) {
setTimeout(() => {
api.start((i) => ({
height: 80,
x: 0,
scale: 0,
delay: i === 1 ? 500 : 0,
}));
flung.clear();
}, 600);
}
}
);

A lot of the code here looks new, right? Indeed! A summary of this code section would be,

  • tracking of state on the dismissibles
  • action to be taken based on user's behaviour via a gesture
  • actual output definition, using the api handler
  • reset of state once list items are flung

Now, lets expand upon the points we discussed during summarization of the section.

  1. State Tracking
const [flung] = useState(() => new Set<number>());

We're using a Set data-structure to hold our state, for dismissed indexes, since the dismiss action should be recorded only once, we use a Set, as it only store unique values. We name it flung (since we're trying to fling away our dismissibles).

  1. Gesture Based Actions
const bind = useDrag(
({
args: [index],
down,
movement: [mx],
velocity: [velocity],
direction: [x],
}) => {
if (!down && velocity > 1 && x === 1) flung.add(index);
}
);

Our useDrag hook is a handler to returns ReactDOMAttributes, this means we can directed use the bind function inside the JSX elements which support ReactDOMAttributes, this is how @use-gesture/react is able to track gestures on the elements.

useDrag gives us a lot of data about the gesture, to define exactly what we want to do, once the gesture is triggered. In our use-case, we want to know about the,

  • movement
  • velocity
  • direction of movement
  • if user is currently interacting with element (via down parameter)

of the gesture in on the x-axis,
Since, bind is a function that will be common across all list items, we need a way to identify which list item is being interacted with, we're doing this by passing our list-item index to bind when calling the function.
Learn more about what properties are available on the useDrag hook here.

With these things defined, we check, if the user is currently interacting with the element, if not, and if the element is flung with a velocity >= 1, we consider the element to be dismissed, thus adding this index to the flung Set.

  1. Output using react-spring
api.start((i) => {
if (flung.has(i)) {
return {
x: 400,
height: 300,
scale: 300,
};
}
if (!flung.has(i) && i === index && springs[i].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,
};
}
});

The api handler is an imperative api for triggering react-spring transitions. It triggers a transition based on the config provided. In our case, we want to trigger different transitions based on the index of the list item.

  • Case 1 (when the list item exists in the flung Set)

    if (flung.has(i)) {
    return {
    x: 400,
    height: 300,
    scale: 300,
    };
    }

    We return a config with these values (more on the values later), flinging off the list item.

  • Case 2 (when the list item is interacted upon by grabbing and moving it)

    if (!flung.has(i) && i === index && springs[i].x.get() >= 0) {
    return {
    x: down ? mx : 0,
    height: down ? mx : 80,
    scale: down ? mx : 0,
    };
    }

    We check if the user is interacting with an item, not present in the flung Set, if so, and he is interacting with the element in the positive x-axis, we apply the movement value onto the element's transition values.

  • Case 3 (when the user releases the grabbed item)

    else if (!down) {
    return {
    x: 0,
    height: 80,
    scale: 0,
    };
    }

    We already have a conditional down ? mx : 0 statement that we use, in case 2, so why this? this is just a clean-up for the side-effects of case 2, so that even if the user flings the element in negative x-axis and releases it, we should reset the value.

  1. State Reset
if (flung.size === 2 && !down) {
setTimeout(() => {
api.start((i) => ({
height: 80,
x: 0,
scale: 0,
delay: i === 1 ? 500 : 0,
}));
flung.clear();
}, 600);
}

We simply check if our flung Set is full, and user is not currently interacting with the list, if that's the case then we reset the state of list items (after a delay of 600ms).

  1. Animation control and Component declaration (Part 2)

return (
<>
{springs.map((props, index) => {
const height = props.height.to({
map: Math.abs,
range: [160, 280],
output: [80, 0],
extrapolate: "clamp",
});

const commonProps = {
borderRadius: 10,
touchAction: "none",
marginBottom: 10,
};

return (
<animated.div
key={index}
{...bind(index)}
style={{
x: props.x,
height,
width: 160,
backgroundColor: index ? "#ff6" : "#ff6d6d",
position: "relative",
...commonProps,
}}
>
<animated.div
style={{
height,
width: 80,
scale: props.scale.to({
map: Math.abs,
range: [0, 280],
output: [0, 1],
extrapolate: "clamp",
}),
backgroundColor: index ? "#ff6d6d" : "#ff6",
position: "absolute",
...commonProps,
}}
/>
</animated.div>
);
})}
</>
);

This code section seems large but is quite simple, we simply apply our useDrag hook handler onto the actual JSX. Along with that we're interpolating our animation values, onto values we actually want the component to display.

Example:

props.scale.to({
map: Math.abs,
range: [0, 280],
output: [0, 1],
extrapolate: "clamp",
});

Here we're defining input range to be between 0 - 200, and output range to be 0 - 1. The mapping function for value is Math.abs, to use the absolute input value. extrapolate is used to tell the component what to do, once we exceed the given input range.
Learn more about interpolation here.

All other properties are exactly the same as we have used before in recipe 1 and recipe 2.

Conclusion

Phew! That was a lot of code and explanation, we covered a lot of concepts in this section, mainly,

  • useSprings hook
  • spring config
  • react-spring api based imperative animation triggers
  • interpolation
  • a bit about gesture control using @use-gesture/react

This tutorial further elaborates how powerful react-spring is, in defining complex animations on components. I hope you enjoyed this tutorial and that this was informative.