React · Lesson 14 of 15
Component Patterns
Compound components, portals, forwarding refs and composition that scales.
- Advanced
- 16 min read
- 3 objectives
Before this lessonLesson 13: App-wide State with Zustand
What you will learn
- Build a compound component
- Use a portal
- Forward a ref
Your Progress
0 of 15 lessons 0%
- Lessons0 / 15
- Completed0
- Est. time left~ 4 hours
Create a free account to keep your progress on every device.
As a UI kit grows, a few composition patterns keep components flexible without a props explosion.
Compound components
Related pieces share state through context, and the parent decides the markup.
import { createContext, useContext, useState } from "react";
const TabsCtx = createContext(null);
export function Tabs({ children, defaultValue }) {
const [value, setValue] = useState(defaultValue);
return <TabsCtx.Provider value={{ value, setValue }}>{children}</TabsCtx.Provider>;
}
Tabs.List = function List({ children }) { return <div role="tablist">{children}</div>; };
Tabs.Tab = function Tab({ id, children }) {
const { value, setValue } = useContext(TabsCtx);
return (
<button role="tab" aria-selected={value === id} onClick={() => setValue(id)}>
{children}
</button>
);
};
Tabs.Panel = function Panel({ id, children }) {
const { value } = useContext(TabsCtx);
if (value !== id) return null;
return <div role="tabpanel">{children}</div>;
};Portals
Modals and toasts should escape overflow and stacking-context traps. createPortal renders children into a different DOM node.
import { createPortal } from "react-dom";
function Modal({ children, onClose }) {
return createPortal(
<div className="overlay" onClick={onClose}>
<div className="dialog" onClick={(e) => e.stopPropagation()}>{children}</div>
</div>,
document.body,
);
}Forwarding refs
A custom input that wraps <input> must forward the ref so a parent can focus it.
import { forwardRef } from "react";
const TextField = forwardRef(function TextField({ label, ...props }, ref) {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
});