Collapsible react component
Create an animated collapsible component is not an easy task. I've tried several approaches and I've finally used the excellent framer-motion library.
## Naive version
The first version is quite straight forward:
import React from "react";
import { motion } from "framer-motion";
const variants = {
open: { height: "auto" },
collapsed: { height: 0 },
};
type Props = {
isOpen: boolean;
};
const Collapsible: React.FC<Props> = ({ isOpen, children }) => (
<motion.div
className="overflow-hidden"
initial={isOpen ? "open" : "collapsed"}
animate={isOpen ? "open" : "collapsed"}
variants={variants}
transition=
>
{children}
</motion.div>
);
export default Collapsible;
The problem here is that the children will be render even if it's not displayed. That could be an issue if you are rendering a lot of components or the component themselves uses side effects (like server request, in my case)
No render children version #
We want to skip children rendering when the panel is collapsed.
That's easy when moving from collapsed to expanded. But the opposite is not so simple, because we need to wait until the panel is fully collapsed.
Here's what I came with:
import React, { useEffect, useState } from "react";
import { motion } from "framer-motion";
type States = "open" | "collapsed";
const variants = {
open: { height: "auto" },
collapsed: { height: 0 },
};
type Props = {
isOpen: boolean;
};
const Collapsible: React.FC<Props> = ({ isOpen, children }) => {
const [hasChildren, setHasChildren] = useState(isOpen);
const [animate, setAnimate] = useState<States>(isOpen ? "open" : "collapsed");
useEffect(() => {
if (isOpen) {
setHasChildren(true);
setAnimate("open");
} else {
setAnimate("collapsed");
const id = setTimeout(() => setHasChildren(false), 300);
return () => clearInterval(id);
}
}, [isOpen, setHasChildren]);
return (
<motion.div
className="overflow-hidden"
initial={animate}
animate={animate}
variants={variants}
transition=
>
{hasChildren && children}
</motion.div>
);
};
export default Collapsible;