Introduction
Honestly speaking, react patterns are kinda weird. The pattern that we use in React doesn't come as naturally. Which is why I created this article. My main goal is to let you know that this pattern exists.
I can't give you an exact answer of when to use these patterns, because it differs case by case. Or as we might say: It depends. After you know that this pattern exists, my hope is when you get the specific use case you'll remember this article, and use it as one of the solutions. So bookmarking this article might be a great idea.
Before you read this article, I want you to know that the example that I provided might be solved with another pattern. You can treat the examples as my way of showing you the syntaxes. At the end of the day, all roads lead to Rome.
There are 4 patterns that I'll cover, you can skip ahead if you like:
Component with Context Pattern
Components that share some UI states could benefit from react context.
Demo App
Video description:
- When the eye button is toggled, both the card title and subtitle change to bullets
This is a pretty simple component, we have a hidden
state that we need to share between the title and subtitle components. The first thing that comes to mind is to simply pass the props, but it might not be the best when the component grows. This is where component with context shines.
Creating a Context
First, we need to create the context. Here's the basic boilerplate that you can use.
import * as React from 'react';
type HidableCardContextType = {
hidden: boolean;
toggle: () => void;
};
const HidableCardContext = React.createContext<HidableCardContextType | null>(
null
);
export function HidableCardContextProvider({
children,
}: {
children: React.ReactNode;
}) {
const [hidden, setHidden] = React.useState(false);
function toggle() {
setHidden((prev) => !prev);
}
return (
<HidableCardContext.Provider value={{ hidden, toggle }}>
{children}
</HidableCardContext.Provider>
);
}
export const useHidableCardContext = () => {
const context = React.useContext(HidableCardContext);
if (!context) {
throw new Error(
'useHidableCardContext must be used inside the HidableCardContextProvider'
);
}
return context;
};
Note:
- I like separating context declaration to another file since multiple child components might share it
- (line 11) Create a context provider for cleaner usage, and for declaring the states we are going to share
- (line 28) Lastly, create a custom hook for using the context. This is where you can add a checker if the component is inside the provider or not.
Composing the Context into a Component
When creating the base component, we can directly integrate the context provider right inside it.
export function HidableCard({
className,
...rest
}: Pick<React.ComponentPropsWithoutRef<'div'>, 'className' | 'children'>) {
return (
<HidableCardContextProvider>
<div
className={cn([
'p-4',
'rounded-lg border border-gray-200 shadow-sm',
className,
])}
{...rest}
/>
</HidableCardContextProvider>
);
}
Then you can create the child components by utilizing the hook that we created
//#region //*=========== Title ===========
export function HidableCardTitle({
className,
children,
}: Pick<React.ComponentPropsWithoutRef<'h3'>, 'className' | 'children'>) {
const { hidden } = useHidableCardContext();
return (
<h3 className={cn(['tracking-tighter text-gray-800', className])}>
{hidden ? <span className='tracking-wide'>••••••••</span> : children}
</h3>
);
}
//#endregion //*======== Title ===========
//#region //*=========== Subtitle ===========
export function HidableCardSubtitle({
className,
children,
}: Pick<React.ComponentPropsWithoutRef<'p'>, 'className' | 'children'>) {
const { hidden } = useHidableCardContext();
return (
<p className={cn(['text-sm text-gray-500', className])}>
{hidden ? <span className='tracking-wide'>••••••••</span> : children}
</p>
);
}
//#endregion //*======== Subtitle ===========
//#region //*=========== Hide Button ===========
export function HidableCardHideButton({ className }: { className?: string }) {
const { hidden, toggle } = useHidableCardContext();
return (
<IconButton
variant='light'
icon={hidden ? EyeOff : Eye}
className={className}
classNames={{ icon: 'text-xs' }}
onClick={toggle}
/>
);
}
//#endregion //*======== Hide Button ===========
What's nice about this is when you use HidableCardTitle
outside of the base component, it will throw out an error since you don't have access to the context. Providing a safe developer experience.
Example Usage
<HidableCard className='flex items-start justify-between'>
<div className='space-y-1'>
<HidableCardTitle>Card Title</HidableCardTitle>
<HidableCardSubtitle>Card Subtitle</HidableCardSubtitle>
</div>
<HidableCardHideButton />
</HidableCard>
Pretty nice right? Make sure to prefix the child component with the base name so it is distinguishable.
Here's the full source code
Bonus: Compound Components
At the end of the file, you can export it this way to make it into a compound component.
export const HidableCard = Object.assign(HidableCard, {
Title: HidableCardTitle,
Subtitle: HidableCardSubtitle,
HideButton: HidableCardHideButton,
});
Then you can call it like so
<HidableCard className='flex items-start justify-between'>
<div className='space-y-1'>
<HidableCard.Title>Card Title</HidableCard.Title>
<HidableCard.Subtitle>Card Subtitle</HidableCard.Subtitle>
</div>
<HidableCard.HideButton />
</HidableCard>
ps: this won't work in react server components.
Compound components pattern is used by Headless UI. It's nice since we only need to import one component and get access to all the child components. However, it's not tree-shakeable. It's fine for Headless UI's case since you'll use all of the components anyway.
Function as Child Pattern
This pattern exposes props and allows you to render custom UI to the component. A bit hard to explain in words, let's just see the example.
Demo App
Video description
- A dialog boolean state which is shown in the text (”Dialog is closed/open”)
- A button that when clicked will set the boolean to true and opens a dialog.
Example Usage
export function GreetingDialogDemo() {
return (
<GreetingDialog>
{({ isOpen, openDialog }) => (
<div>
<div>
Dialog is{' '}
{isOpen ? (
<span className='text-green-500'>opened</span>
) : (
<span className='text-red-500'>closed</span>
)}
</div>
<Button className='mt-4' variant='dark' onClick={openDialog}>
click me!
</Button>
</div>
)}
</GreetingDialog>
);
}
As you can see on line 4, the states inside are exposed to the children—where you can render anything you like based on the state.
When dealing with this kind of behavior, it's natural to put the state outside of the component like so:
export function UsualWayDemo() {
const [isOpen, setIsOpen] = React.useState(false);
return (
<GreetingDialog isOpen={isOpen} setIsOpen={setIsOpen}>
{isOpen && ...custom ui here}
</GreetingDialog>
)
}
Well this works, but you now have something I like to call a hidden convention.
A hidden convention is when you have to fulfill a requirement to use a component. Which in this case is declaring the isOpen
state and passing it to the component.
With function as a child pattern, the state lives inside the component itself, and can be accessed by the children. Amazing.
Implementation
'use client';
import * as React from 'react';
import { PiHandWaving } from 'react-icons/pi';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
type ReturnProps = {
isOpen: boolean;
openDialog: () => void;
};
export default function GreetingDialog({
children,
}: {
children: (props: ReturnProps) => React.ReactNode;
}) {
const [open, setOpen] = React.useState(false);
function openDialog() {
setOpen(true);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
{/* If you're using radix primitives, you could actually just use <DialogTrigger asChild>{children}</DialogTrigger> */}
{/* However, this pattern is useful if the children need to access states inside the component e.g. isOpen */}
{children({ openDialog, isOpen: open })}
<DialogContent>
<div className='flex items-center gap-2'>
<PiHandWaving className='animate-waving text-yellow-500' size={30} />
<DialogTitle>Hello from a dialog! </DialogTitle>
</div>
</DialogContent>
</Dialog>
);
}
ps: incompatible with react server components, you need to create a client wrapper
Note:
- The only special thing in the code is in line 28. Where instead of only returning
{children}
, we now returnchildren({props})
- For the children type, you need to change it into a function that returns
ReactNode
Here's the full source code
Forward Ref Pattern
React components do not expose ref by default. While it is a bummer, it kinda makes sense since it's quite rare that we need to access the ref of a component.
When you need to access them, we need to do ref forwarding. This does not completely fit to be categorized as a pattern since it is a feature of React, but I want to include this because it's uncommon.
Demo App
Description:
Logs showing two different results of accessing ref between components:
- SimpleButton is not forwarded, hence resulting null
- The button with forwardRef has the ref value
Why bother forwarding?
I can't fully cover the use of ref in this article, usually, it's used when you are building a basic design system element like buttons or links.
As I said, it's rare that you directly access the ref yourself. However, component libraries like Radix and Headless UI do access them. If you don't forward your ref, the tooltip from Radix UI won't work because they rely on the trigger ref.
<TooltipTrigger asChild>
{/* Does not pass ref */}
<SimpleButton />
</TooltipTrigger>
This is why if you are creating a button for the design system, forwarding the ref is mandatory.
Implementation
import * as React from 'react';
import { cn } from '@/lib/utils';
/** Notice the use of ComponentProps_With_Ref */
type ButtonWithRefProps = React.ComponentPropsWithRef<'button'>;
// forward ref
export const ButtonWithRef = React.forwardRef<
HTMLButtonElement,
ButtonWithRefProps
>(({ className, ...rest }, ref) => (
<button
ref={ref}
className={cn(['underline active:no-underline', className])}
{...rest}
/>
));
To forward a ref, you can just use React.forwardRef
function, and follow the syntax above
Here's the full source code
Higher-Order Components Pattern
A higher-order component (HOC) allows us to inject props or do side effects to a component.
Demo App
Description:
A DummyComponent uses withLogger
HOC, which will automatically log the props passed to the component.
Implementation
Higher-order components usually start with withFunctionality
naming convention.
import * as React from 'react';
import logger from '@/lib/logger';
type WithLoggerProps = object;
/**
* For more TypeScript syntax check out the cheatsheet:
* @see https://react-typescript-cheatsheet.netlify.app/docs/hoc/full_example/ */
export function withLogger<T extends WithLoggerProps = WithLoggerProps>(
WrappedComponent: React.ComponentType<T>
) {
const displayName =
WrappedComponent.displayName || WrappedComponent.name || 'Component';
const Component = (props: Omit<T, keyof WithLoggerProps>) => {
React.useEffect(() => {
logger(props, `Component Props: ${displayName}`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <WrappedComponent {...(props as T)} />;
};
Component.displayName = `withLogger(${displayName})`;
return Component;
}
Note:
- This HOC will automatically log the props for every initial render using useEffect
logger
function is a simple wrapper that nicely console.log
Example Usage
function DummyComponent({
content,
number,
}: {
content: string;
number: number;
}) {
return (
<p className='font-mono text-sm'>
{`<DummyComponent content={'${content}'} number={${number}} />`}
</p>
);
}
export default withLogger(DummyComponent);
Fairly simple and clean usage. With HOC, your logs don't even need to be inside the component. It will be injected automatically using the HOC.
Here's the full source code
If you're interested in seeing more intricate examples, I have an article about Next.js Authentication using Higher-Order Components.
Conclusion
Learn anything new?
There are a lot of react patterns out there, and you don't need to memorize all of them. My best advice is to at least remember they exist. Trust me, when you see the perfect use case for it, you'll know how to use it with some syntax guidance.
Here are some more patterns that you can check out:
- Prop getters
- Render props
I don't cover them because I haven't found a good example for it. I'll update this article when I do. You can subscribe to get notified.