Skip to main content

(III) HOC Pattern

Motivation

This section seems new, thats true, since we've already built our base components, we could very well take these components, and build out a complex and interactive UI from it, and it would work. But its not optimal. The scoping we've followed, shows how big our codebase can become, if we wanted to coordinate and control these animations to be in a flow. This, is not ideal, and so we're gonna try to make our components into HOCs, the hard way, to gain the most knowledge possible, from it.

Code Architecture

  • Earlier when we built our components, it was a one-track process

    1. we decided what animated properties our component needs
    2. we used the right kind of animation controller to modify the required property
    3. we assigned our component, those properties

and voila! we were done, as it turns out, this is suitable only in small projects or with isolated components. In real-world projects, components usually talk or react to one another, and are supposed to follow the SOLID principles.

  • We're gonna try to make our component feel as standalone as possible, while making it reusable (isn't react all about reusability?), this requires us to follow a very strategic yet interesting process

    1. Define what the base component is. (eg: it is a container that shrinks and grows)
    2. Define the parameters you want to be able to modify in the component, via props and refs (we will try to modify just about everything)
    3. implement the component using this design philosophy.

What we're gonna be building (Part 1)


As a side-note, we'll be building our component using typescript,this will mean making our component logic a bit more complex, but trust me, its better to write it this way, in the long run.

Code

type Props = {
expand?: boolean;
} & PropsWithChildren;

const ShrinkGrow = forwardRef(
(props: Props, shrinkGrowRef: SpringRef<Lookup<any>>) => {
const { scale } = useSpring({
ref: shrinkGrowRef,
from: {
scale: props.expand ? 0 : 1,
},
to: {
scale: props.expand ? 1 : 0,
},
});
return (
<animated.div
style={{
width: 80,
background: "#ff6",
height: 80,
borderRadius: 8,
scale,
}}
>
{props.children}
</animated.div>
);
}
);
export default ShrinkGrow;

Code Breakdown

  1. Props Defintion

type Props = {
expand?: boolean;
} & PropsWithChildren;

For people from the non-typescript world, this is just a static type declaration of what our props is allowed to accept, it helps with linting, prompts and auto-completion. In our case, we're letting the user pass.

  • expand (an abstract animation control via boolean)
  • children (to allow our component to be a parent component for any components, this component needs to wrap)
  • ref (this is defined by the forwardRef)
  1. HOC wrapper

const ShrinkGrow = forwardRef(
(props: Props, shrinkGrowRef: SpringRef<Lookup<any>>) => {}
);

Since our component is meant to be a HOC, we need to wrap our functional component with a forwardRef, i.e. a forwardRef renderFunction, this helps pass down a reference, via props which we can use in our function component.
(this is useful for tracking our component's animation state and/or trigger orchestrated animations).
learn more about HOC and forwardRef here.

The type of ref we want to accept is a SpringRef (a special type of ref object defined by react-spring), this is because we want our animation controller to have access to this reference, so that the reference can keep a track of the current animation process.
(but why? you will know soon enough)

  1. Animation definition

const { scale } = useSpring({
ref: shrinkGrowRef,
from: {
scale: props.expand ? 0 : 1,
},
to: {
scale: props.expand ? 1 : 0,
},
});

The animation definition, for the most part, remains the same as the first and second recipe, except we're now assigning a ref, to keep a track of this hook's state modification, as ref: shrinkGrowRef. Everything else, remains exactly the same as the earlier recipes. (try converting recipe 1, to follow an HOC architecture)

The Main Question!

We added a ref handle, but why? We could've easily controlled our animation via props, so why the ref? As you know, refs are an escape hatch, from the react rules and system. This allows react-spring to control animation properties, without having to define every property via useSpring, for each component. It also allows react-spring to take control of the animation system over a larger set of components, and coordinate the animations, to portray them in a way that we want, without having to do a lot of heavy lifting ourselves.

What we're gonna be building (Part 2)

Now that we have our components laid out in an HOC architecture, we're ready to use some more tools provided by react-spring, meant to make life easier, while animating components using React. One such tool is going to be useChain, that is used in parallel with useSpringRef. (Hence the initial setup via the HOC architecture)

Some Explanation

As you can see, the above example transition for ShrinkGrow lags behind the SlideAround by some amount (0.4s to be precise). We're going to achieve this effect, using useChain, detailed explanation will be provided in Code Breakdown.
Make sure you've read and understand these recipes and sections before proceeding further :-

Code

const ReactSpringTransition = () => {
const translationSpringRef = useSpringRef();
const scaleSpringRef = useSpringRef();
const [animate, setAnimate] = useState(false);

useEffect(() => {
const timeout = setTimeout(() => {
setAnimate(!animate);
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [animate]);

useChain([scaleSpringRef, translationSpringRef], [0.4, 0], 1000);

return (
<>
<SlideAround move={animate} ref={translationSpringRef} />
<ShrinkGrow expand={animate} ref={scaleSpringRef} />
</>
);
};

Code Breakdown

  1. The useSpringRef

const translationSpringRef = useSpringRef();
const scaleSpringRef = useSpringRef();

When we converted our components to use HOC architecture, we wrapped them with a forwardRef function, later accessed this ref via the component's useSpring hook. As we discussed, this is to store and modify the useSpring hook behaviour. For this to work, react-spring provides a hook, useSpringRef, which returns a ref, that works in tandem with useSpring to track and control the transitions on the defined component.
Learn more about the SpringRef here.

  1. Animation Configuration and Control

const [animate, setAnimate] = useState(false);

useEffect(() => {
const timeout = setTimeout(() => {
setAnimate(!animate);
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [animate]);

This is the process we have repeated for each component, and is a simple timeout operation, where we're changing the initial and final state to be transitioned to and from. For detailed explanation, checkout Recipe 1.

  1. The useChain

useChain([scaleSpringRef, translationSpringRef], [0.4, 0], 1000);

Now we're getting to the meaty bit of this code, as you saw, our ShrinkGrow component, lags behind SlideAround, by 0.4s. useChain is responsible for this effect, here we're passing useChain our assigned refs (scaleSpringRef and translationSpringRef), with a transition delay for each ref (0 and 0.4). Along with this, we're also specifying a duration for the entire transition group (of 1 second).
This forces our components to be controlled by useChain, thereby allowing configuration of these components to be defined by useSpring, but the actual orchestration of transitions, to be done by useChain.
Learn more about useChain here.

  1. Component Defintion

return (
<>
<SlideAround move={animate} ref={translationSpringRef} />
<ShrinkGrow expand={animate} ref={scaleSpringRef} />
</>
);

We simply use our pre-existing HOC components, and assign them the valid refs and animation control props.
(NOTE I've recreated the SlideAround, and included a move prop using the same process as for ShrinkGrow).

Conclusion

And Voila! Now we can orchestrate complex transitions, to create beautiful user experiences (as we will in later recipes).
react-spring takes care of all the heavy lifting for us.