React Snippets
This page will be a general discussion of snippets that I find interesting or useful. I think a good way to think of this page is a running list of items that I have liked over the years. It won't be culled or curated much - use React Recipes for items that will be updated and managed.
General
I've had this problem before and don't remember how I've solved it but it's frustrating. This worked well.
const sidebar = useRef(null)
const [sidebarHeight, setSidebarHeight] = useState(0);
useEffect(() => {
const height = sidebar.current.clientHeight;
setSidebarHeight(height);
}, []);
// Element
return (
<div ref={sidebar}>
<div style={{ height: sidebarHeight }}>
<Sidebar />
</div>
</div>
)
Advanced
Render Props useEffect: Headless UI
I was trying to solve an issue where I needed to know whether a Popover was open or not. If it was open, I'd refetch my query.
This is Solution #1:
import { Popover } from '@headlessui/react';
import { useQuery } from '@apollo/client';
import { USER_PROFILE_QUERY } from './queries';
import { useEffect } from 'react';
function UserProfileMenu() {
const { data, refetch } = useQuery(USER_PROFILE_QUERY, {
skip: true, // Don't auto-run on mount
});
return (
<Popover>
{({ open }) => {
useEffect(() => {
if (open) {
refetch();
}
}, [open]);
return (
<>
<Popover.Button>
{/* Avatar, initials, etc. */}
<span>Profile</span>
</Popover.Button>
<Popover.Panel className="z-50 mt-2 rounded-lg shadow-lg bg-white">
{/* You can safely use `data` here */}
<div className="p-4">
{data ? (
<p>Hello, {data.user.name}!</p>
) : (
<p>Loading...</p>
)}
</div>
</Popover.Panel>
</>
);
}}
</Popover>
);
}
This issue with this is that using a useEffect
inside the render props pattern is not
always the most stable. My second solution was to use state at the top-level and then
set a variable popoverIsOpen
inside the render props. Again, not a super stable pattern
because you are setting state inside the render.
This is what I ended up with:
import { Popover } from '@headlessui/react';
import { useQuery } from '@apollo/client';
import { USER_PROFILE_QUERY } from './queries';
import { useEffect } from 'react';
function UserProfileMenu() {
const { data, refetch } = useQuery(USER_PROFILE_QUERY, {
skip: true, // Don't auto-run on mount
});
return (
<Popover>
{({ open }) => {
return (
<>
<RefetchProfile refetch={refetch}>
<Popover.Button>
<span>Profile</span>
</Popover.Button>
<Popover.Panel className="z-50 mt-2 rounded-lg shadow-lg bg-white">
<div className="p-4">
{data ? (
<p>Hello, {data.user.name}!</p>
) : (
<p>Loading...</p>
)}
</div>
</Popover.Panel>
</>
);
}}
</Popover>
);
}
const RefetchProfile = ({ refetch }) => {
useEffect(() => {
refetch()
}, [refetch])
return null
}
We then had to refactor it even more and instead of rendering that little component that would
just refetch, we added unmount
to the Popover.Panel which when it is not displayed, is no
longer in the DOM. Then contents of the popover were in a separate component and we passed
refetch in as a prop.
Passing Props to Dynamic Child
I was working on a feature where there were 6 different types of the same thing. Each of the different types have a title and must query the backend. I decided to create a parent component where I show the title, make the query, and then pass the results of that query to the children of that component. So the problem I'm trying to solve is passing props to an unknown child. The child is "unknown" in that the children component could be one of 6 different components. I'm going to discuss some of the solutions and then show what I ultimately landed on.
First, this is how I use my parent component:
const App = () => {
return (
<div>
<ParentComponent type="type-1" title="Type 1" >
<Type1>
</ParentComponent>
<ParentComponent type="type-2" title="Type 2">
<Type2>
</ParentComponent>
<ParentComponent type="type-3" title="Type 3">
<Type3>
</ParentComponent>
</div>
)
}
Each ParentComponent
takes a title
and type
. The title will be just a h1
element while the type will
be a variable passed to the query.
How do I build the ParentComponent
to pass the data down to any child component?
Here is Version 1: React.cloneElement
// ParentComponent.tsx
export const ParentComponent = ({
children,
title,
type,
}: ParentComponentProps) => {
const { data, loading, error } = useAwesomeFeature({ type })
if (loading) {
return <div>Loading...</div>
}
if (error || !children) {
return <ErrorComponent error={error} />
}
return (
<section>
<h1 className="text-style-xl-bold">{title}</h1>
{React.cloneElement(children, { data })}
</section>
)
The key part is near the bottom: {React.cloneElement(children, { data })}
. This pattern clones the element
and allows you to add props. It actually does appear to be a commonly used pattern but I found that it is more
of a legacy React API and not something they recommend. There are some pitfalls as listed
here and I was getting pushback from other devs.
Here is Version 2: Render Props
I first need to update how we use the ParentComponent
at the top level:
const App = () => {
return (
<div>
<ParentComponent type="type-1" title="Type 1" render={(data) => <Type1 data={data}>} >
<Type1>
</ParentComponent>
{/* etc */}
</div>
)
}
And then tweak how we are returning the children:
export const ParentComponent = ({
render,
title,
type,
}: ParentComponentProps) => {
const { data, loading, error } = useAwesomeFeature({ type });
if (loading) {
return <div>Loading...</div>;
}
if (error || !children) {
return <ErrorComponent error={error} />;
}
return (
<section>
<h1>{title}</h1>
{render(data)}
</section>
);
};
But I really dislike how the render-props looks.
I ended up just going for React Context which I really like.
forwardRef
import React, { forwardRef } from 'react';
import { twMerge } from 'tailwind-merge';
type BadgeProps = {
children?: React.ReactNode;
variant?: 'primary' | 'secondary';
className?: string;
};
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
({ children, variant = 'primary', className, ...props }, ref) => {
return (
<div
{...props}
className={twMerge(
'inline-block px-3 py-1 rounded-full text-sm font-semibold',
variant === 'primary' ? 'bg-blue-500 text-white' : 'bg-gray-500 text-black',
className
)}
ref={ref}
>
{children}
</div>
);
}
) as React.ForwardRefExoticComponent<React.PropsWithoutRef<BadgeProps> & React.RefAttributes<HTMLDivElement>>;
- This example uses
forwardRef
which is a utility that lets you pass a ref through a component to one of its children. I don't use refs often but good to know. - The imports make sense and are readable
- React and
forwardRef
from React twMerge
allows you to merge Tailwind CSS classes
- React and
BadgeProps
are basic and make sense- Let's look at the first part of the code:
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(...)
- This is where I got tripped up looking at the code.
forwardRef
only has one argument, a render function. That render function takes two arguments, props (in this caseBadgeProps
) and the ref. Where I was getting confused was seeing what looked like theref
first and then theBadgeProps
. But really, what you are seeing in the code above is one argument, the render function. - What we are NOT seeing is the implementation of a
<Badge />
so we aren't seeing what's being passed in as the ref. The typesHTMLDivElement, BadgeProps
are just providing the types for the Badge and the ref. As a quick aside, let's look at a basic use of the Badge component:
import React, { useRef } from 'react';
import { Badge } from './Badge';
const App = () => {
const badgeRef = useRef<HTMLDivElement>(null);
return (
<div>
<Badge ref={badgeRef} variant="primary" className="my-badge">Primary Badge</Badge>
</div>
);
};
export default App;
- We create the
badgeRef
and pass that to the<Badge />
. The variant and any other props are passed into the render function so we can use them. - Going back to the original code, now that we understand how
forwardRef
works, the rest of the code makes more sense (original code with comments):
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
// Render function
(
// Badge props broken out so that I can use them
{ children, variant = 'primary', className, ...props },
// the ref
ref
) => {
return (
<div
{...props}
className={twMerge(
'inline-block px-3 py-1 rounded-full text-sm font-semibold',
variant === 'primary' ? 'bg-blue-500 text-white' : 'bg-gray-500 text-black',
className
)}
ref={ref} // here were are using the ref
>
{children}
</div>
);
}
) as React.ForwardRefExoticComponent<React.PropsWithoutRef<BadgeProps> & React.RefAttributes<HTMLDivElement>>;
- The first argument in the render function is just my
BadgeProps
and me breaking them out so I can use them. - the second argument is the
ref
that I'm passing from my parent component (or wherever I'm using it) to the element I want the ref on. In this case, just div of the<Badge />
. - Last bit of code:
(...) as React.ForwardRefExoticComponent<React.PropsWithoutRef<BadgeProps> & React.RefAttributes<HTMLDivElement>>;
as React.ForwardRefExoticComponent<React.PropsWithoutRef<BadgeProps> & React.RefAttributes<HTMLDivElement>>
is a TypeScript type assertion. It explicitly tells TypeScript the exact type of theBadge
component created usingforwardRef
.ForwardRefExoticComponent
is a type provided by React to define components created usingforwardRef
- Inside that is just
PropsWithoutRef
which is a utility type that removes theref
property from the props if it exists. This is done because the ref is handled separately byforwardRef
. - and then finally
RefAttributes
is a utility type which includes theref
property withHTMLDivElement
being the type of the DOM element the ref will refer to. - Assuming we import those types, it would look like this:
) as ForwardRefExoticComponent<PropsWithoutRef<BadgeProps> & RefAttributes<HTMLDivElement>>;
- I think the most confusing part for me was the TypeScript. Understanding how to type (as in TypeScript) all this looks complicated but most of it is clearly boilerplate TS that is added onto your component.