In the following code, I wanted to put all the collapsible logic into the Collapsible
component. However, the collapse is being triggered by a button that’s outside of Collapsible
. So I made Collapsible
to react to that button’s onClick
. For that, I had to use useEffect
:
// JS:
const [collapsed, setCollapsed] = useState(true);
// JSX:
<div className="flex w-full justify-between">
<Heading size="sm" className="shrink-0">
Tags
</Heading>
<Button
variant="link"
onClick={() => setCollapsed(!collapsed)}
className="md:hidden"
>
{collapsed ? (
<IconChevronDown size={20} />
) : (
<IconChevronUp size={20} />
)}
</Button>
</div>
<Collapsible collapsed={collapsed} className="md:hidden">
<div className="mt-4 w-full">{renderedTags}</div>
</Collapsible>
Inside Collapsible
, I have this:
useEffect(() => {
if (collapsed) {
setMaxHeight('0px');
} else if (contentRef.current) {
setMaxHeight(`${contentRef.current.scrollHeight}px`);
}
}, [collapsed]);
Do you think using useEffect
like this to react to user input is bad practice? To prevent that, I could create a useCollapsible
hook, but that would separate the collapsible logic from the Collapsible
component.
2
Answers
useEffect
can be applied here, but React provides a more efficient mechanism for reacting to changes in a prop: callingset
functions during rendering. When you set state while the component is rendering, React will skip rendering the children and immediately rerender the component after the function returns.This could be implemented like so:
Do you think using
useEffect
like this to react to user input is bad practice?Yes.
useEffect
at all.Per to the docs:
useEffect
is executed after the component completes rendering. This will introduce layout shift in your example, the component will render with the previous value ofmaxHeight
then it will rerender again with the new value. (useLayoutEffect
solves this).