React Patterns Cheatsheet
reactCommon React component patterns with TypeScript.
Component Control Patterns
const [value, setValue] = useState<T>(initial); <Input value={value} onChange={setValue} />Parent component controls the state via props - use when you need to validate, transform, or sync input state
const [email, setEmail] = useState('');
<input value={email} onChange={e => setEmail(e.target.value)} />onChange={(e) => { if(validate(e.target.value)) setValue(e.target.value) }}Add validation logic in the change handler before updating state
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (/^\d*$/.test(e.target.value)) setNum(e.target.value);
};const ref = useRef<HTMLInputElement>(null); <input ref={ref} defaultValue={initial} />DOM holds the state - use for simple forms, file inputs, or integrating non-React code
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => console.log(inputRef.current?.value);<form onSubmit={handleSubmit}><input name="field" defaultValue="" /></form>Access values via FormData API on submit rather than tracking each field
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
const data = new FormData(e.currentTarget);
};const isControlled = value !== undefined; const val = isControlled ? value : internalValue;Component works both controlled and uncontrolled based on whether value prop is provided
function Input({ value, defaultValue }: Props) {
const [internal, setInternal] = useState(defaultValue);
const val = value ?? internal;
}Composition Patterns
<Parent><Parent.Child /></Parent>Related components share implicit state - use for flexible UI like tabs, accordions, dropdowns
<Select>
<Select.Option value="a">A</Select.Option>
<Select.Option value="b">B</Select.Option>
</Select>Parent.Child = function Child() { const ctx = useContext(ParentContext); }Use context to share state between compound component parts
const TabsContext = createContext<TabsState | null>(null);
Tabs.Panel = ({ id, children }) => {
const { activeId } = useContext(TabsContext)!;
return activeId === id ? children : null;
};<Component render={(state) => <Child {...state} />} />Pass a function as prop that receives state and returns JSX - use for reusable behavior logic
<MouseTracker render={({ x, y }) => (
<span>Position: {x}, {y}</span>
)} /><Component>{(state) => <Child {...state} />}</Component>Variation of render props using children prop - cleaner syntax for single render function
<Toggle>{({ on, toggle }) => (
<button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
)}</Toggle>interface Props<T> { data: T; render: (item: T) => ReactNode; }Type the render function parameter to ensure type safety in consumer components
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
}interface Props { header?: ReactNode; footer?: ReactNode; children: ReactNode; }Named slots for flexible layout composition without deep nesting
<Card
header={<h2>Title</h2>}
footer={<Button>Save</Button>}
>{content}</Card>Higher-Order Components
function withFeature<P>(Component: ComponentType<P>): FC<Omit<P, 'feature'>>Function that takes a component and returns enhanced component - use for cross-cutting concerns
function withLogger<P>(Wrapped: ComponentType<P>) {
return (props: P) => { useEffect(() => console.log('mounted')); return <Wrapped {...props} />; };
}const withFeature = (config: Config) => <P,>(Component: ComponentType<P>) => FC<P>Curried HOC that accepts configuration before the component
const withTheme = (theme: Theme) => <P,>(Comp: ComponentType<P & ThemeProps>) =>
(props: P) => <Comp {...props} theme={theme} />;type InjectedProps = { user: User }; type Props<P> = Omit<P, keyof InjectedProps>;Properly type props that HOC injects to avoid requiring them from consumers
interface WithUserProps { user: User; }
function withUser<P extends WithUserProps>(Comp: ComponentType<P>) {
return (props: Omit<P, keyof WithUserProps>) => <Comp {...props as P} user={currentUser} />;
}WrappedComponent.displayName = `withFeature(${Component.displayName || Component.name})`;Set displayName for better debugging in React DevTools
const Enhanced = (props: P) => <Wrapped {...props} />;
Enhanced.displayName = `withAuth(${Wrapped.displayName || 'Component'})`;const withFeature = <P,>(Comp: ComponentType<P>) => forwardRef<Ref, P>((props, ref) => ...)Forward refs through HOC to allow parent access to underlying DOM element
const withTooltip = <P,>(Comp: ComponentType<P>) =>
forwardRef<HTMLElement, P>((props, ref) => <Comp {...props} ref={ref} />);Custom Hooks Patterns
function useCustom<T>(param: T): [T, (val: T) => void]Extract reusable stateful logic - always prefix with use to enable Rules of Hooks
function useToggle(initial = false) {
const [state, setState] = useState(initial);
const toggle = useCallback(() => setState(s => !s), []);
return [state, toggle] as const;
}useEffect(() => { subscribe(); return () => unsubscribe(); }, [deps]);Return cleanup function from useEffect to prevent memory leaks
function useWindowSize() {
const [size, setSize] = useState({ w: innerWidth, h: innerHeight });
useEffect(() => { const h = () => setSize({...}); addEventListener('resize', h); return () => removeEventListener('resize', h); }, []);
}return { value, setValue, reset, isValid } as const;Return object for named access to multiple values - easier to consume selectively
function useForm<T>(initial: T) {
const [values, setValues] = useState(initial);
const reset = () => setValues(initial);
return { values, setValues, reset };
}function useFetch<T>(url: string): { data: T | null; loading: boolean; error: Error | null }Generic type parameter allows hook to work with any data shape
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
useEffect(() => { fetch(url).then(r => r.json()).then(setData); }, [url]);
return data;
}const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, initialState);Use for complex state logic with multiple sub-values or next state depends on previous
type Action = { type: 'inc' } | { type: 'dec' };
const reducer = (state: number, action: Action) =>
action.type === 'inc' ? state + 1 : state - 1;const [state, setState] = useState<T>(() => computeExpensiveInitial());Pass function to useState/useReducer for expensive initial state computation
const [items, setItems] = useState(() => JSON.parse(localStorage.getItem('items') || '[]'));Context Pattern
const MyContext = createContext<ContextType | null>(null);Use null default with type union to catch missing provider errors
interface AuthContext { user: User | null; login: (u: User) => void; }
const AuthCtx = createContext<AuthContext | null>(null);function Provider({ children }: { children: ReactNode }) { return <Ctx.Provider value={val}>{children}</Ctx.Provider>; }Wrap context provider in custom component to encapsulate state logic
function AuthProvider({ children }: PropsWithChildren) {
const [user, setUser] = useState<User | null>(null);
return <AuthCtx.Provider value={{ user, login: setUser }}>{children}</AuthCtx.Provider>;
}function useMyContext() { const ctx = useContext(MyContext); if (!ctx) throw new Error('Missing Provider'); return ctx; }Custom hook that throws if used outside provider - better DX than silent null
function useAuth() {
const ctx = useContext(AuthCtx);
if (!ctx) throw new Error('useAuth must be within AuthProvider');
return ctx;
}const StateCtx = createContext<State>(init); const DispatchCtx = createContext<Dispatch>(noop);Separate state and dispatch contexts to prevent unnecessary re-renders
const CountStateCtx = createContext(0);
const CountDispatchCtx = createContext<Dispatch<Action>>(() => {});const value = useSyncExternalStore(subscribe, () => selector(getSnapshot()));Use external store pattern to select partial context and avoid re-renders
function useContextSelector<T, S>(ctx: Context<T>, selector: (v: T) => S): S {
const value = useContext(ctx);
return useMemo(() => selector(value), [value, selector]);
}Performance Patterns
const Memoized = memo<Props>(Component, (prev, next) => prev.id === next.id);Skip re-render if props unchanged - use for expensive components with stable props
const ExpensiveList = memo(function List({ items }: { items: Item[] }) {
return items.map(i => <Row key={i.id} item={i} />);
});const MemoGeneric = memo(function <T>(props: Props<T>) { ... }) as <T>(props: Props<T>) => JSX.Element;Type assertion needed to preserve generic with memo wrapper
const MemoList = memo(function <T>({ items, render }: ListProps<T>) {
return <>{items.map(render)}</>;
}) as <T>(props: ListProps<T>) => JSX.Element;const computed = useMemo<T>(() => expensiveCalc(deps), [deps]);Memoize expensive calculations - only recompute when dependencies change
const sortedItems = useMemo(() =>
[...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]);const handler = useCallback<(e: Event) => void>((e) => { ... }, [deps]);Memoize callbacks to maintain referential equality for memo children
const handleClick = useCallback((id: string) => {
setSelected(id);
}, []);const actions = useMemo(() => ({ inc: () => dispatch('inc'), dec: () => dispatch('dec') }), []);Wrap multiple callbacks in useMemo object to pass single stable reference
const api = useMemo(() => ({
save: () => mutate(data),
reset: () => setData(initial)
}), [data]);Ref Patterns
const Comp = forwardRef<HTMLInputElement, Props>((props, ref) => <input ref={ref} {...props} />);Forward ref to child element - use for reusable input components needing imperative focus
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} className="input" {...props} />
));useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }), []);Customize ref value exposed to parent - limit imperative API surface
interface Handle { focus: () => void; clear: () => void; }
const Input = forwardRef<Handle, Props>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), clear: () => { if(inputRef.current) inputRef.current.value = ''; } }));
});<div ref={(node) => { if (node) measureRef.current = node; }} />Function ref called with element - use when you need to react to ref attachment
const [height, setHeight] = useState(0);
const measureRef = useCallback((node: HTMLDivElement | null) => {
if (node) setHeight(node.getBoundingClientRect().height);
}, []);const mergeRefs = <T>(...refs: Ref<T>[]) => (node: T) => refs.forEach(r => { if(typeof r === 'function') r(node); else if(r) r.current = node; });Combine multiple refs into one - use when component needs internal ref and forwards ref
const Button = forwardRef<HTMLButtonElement, Props>((props, extRef) => {
const intRef = useRef<HTMLButtonElement>(null);
return <button ref={mergeRefs(intRef, extRef)} {...props} />;
});Error & Async Patterns
class ErrorBoundary extends Component<Props, { error: Error | null }> { static getDerivedStateFromError(e: Error) { return { error: e }; } }Catch JavaScript errors in child tree - must be class component
class ErrorBoundary extends Component<PropsWithChildren, { error: Error | null }> {
state = { error: null };
static getDerivedStateFromError(e: Error) { return { error: e }; }
render() { return this.state.error ? <Fallback /> : this.props.children; }
}componentDidCatch(error: Error, info: ErrorInfo) { logError(error, info); }Log errors and provide recovery mechanism for users
<ErrorBoundary
fallback={({ error, reset }) => (
<div><p>{error.message}</p><button onClick={reset}>Retry</button></div>
)}
>{children}</ErrorBoundary><Suspense fallback={<Loading />}><LazyComponent /></Suspense>Show fallback while children are loading - use with lazy components or data fetching libs
const LazyDashboard = lazy(() => import('./Dashboard'));
<Suspense fallback={<Spinner />}><LazyDashboard /></Suspense><ErrorBoundary fallback={<Error />}><Suspense fallback={<Loading />}><Async /></Suspense></ErrorBoundary>Combine error and loading states for complete async UI handling
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<Skeleton />}>
<UserProfile />
</Suspense>
</ErrorBoundary><Suspense fallback={<Shell />}><Header /><Suspense fallback={<ContentSkeleton />}><Content /></Suspense></Suspense>Nested boundaries allow progressive loading of different UI sections
<Suspense fallback={<PageSkeleton />}>
<Nav />
<Suspense fallback={<ListSkeleton />}><ItemList /></Suspense>
</Suspense>Portal Pattern
createPortal(children, document.getElementById('portal-root')!)Render children into DOM node outside parent hierarchy - use for modals, tooltips, dropdowns
function Modal({ children }: PropsWithChildren) {
return createPortal(children, document.getElementById('modal-root')!);
}const [container] = useState(() => document.createElement('div'));Create container element in state to ensure stable reference across renders
function Portal({ children }: PropsWithChildren) {
const [el] = useState(() => document.createElement('div'));
useEffect(() => { document.body.appendChild(el); return () => { document.body.removeChild(el); }; }, [el]);
return createPortal(children, el);
}return isOpen ? createPortal(<Modal onClose={close} />, container) : null;Only create portal when needed to avoid unnecessary DOM nodes
function Tooltip({ show, children, anchor }: Props) {
if (!show) return null;
return createPortal(<div style={getPosition(anchor)}>{children}</div>, document.body);
}<div onClick={handleParentClick}><Portal><button onClick={handlePortalClick} /></Portal></div>Events still bubble through React tree even though DOM is separate
// Click in portal still triggers parent onClick in React tree
<div onClick={() => console.log('parent')}>
<Portal><button>Click</button></Portal>
</div>