{ ILoveJS }

React Patterns Cheatsheet

react

Common React component patterns with TypeScript.

9 sections · 45 items

Component Control Patterns

Controlled Component
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

typescript
const [email, setEmail] = useState('');
<input value={email} onChange={e => setEmail(e.target.value)} />
Controlled with Validation
onChange={(e) => { if(validate(e.target.value)) setValue(e.target.value) }}

Add validation logic in the change handler before updating state

typescript
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
  if (/^\d*$/.test(e.target.value)) setNum(e.target.value);
};
Uncontrolled Component
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

typescript
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => console.log(inputRef.current?.value);
Uncontrolled with Form
<form onSubmit={handleSubmit}><input name="field" defaultValue="" /></form>

Access values via FormData API on submit rather than tracking each field

typescript
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
  const data = new FormData(e.currentTarget);
};
Mixed Control Pattern
const isControlled = value !== undefined; const val = isControlled ? value : internalValue;

Component works both controlled and uncontrolled based on whether value prop is provided

typescript
function Input({ value, defaultValue }: Props) {
  const [internal, setInternal] = useState(defaultValue);
  const val = value ?? internal;
}

Composition Patterns

Compound Components
<Parent><Parent.Child /></Parent>

Related components share implicit state - use for flexible UI like tabs, accordions, dropdowns

typescript
<Select>
  <Select.Option value="a">A</Select.Option>
  <Select.Option value="b">B</Select.Option>
</Select>
Compound with Context
Parent.Child = function Child() { const ctx = useContext(ParentContext); }

Use context to share state between compound component parts

typescript
const TabsContext = createContext<TabsState | null>(null);
Tabs.Panel = ({ id, children }) => {
  const { activeId } = useContext(TabsContext)!;
  return activeId === id ? children : null;
};
Render Props
<Component render={(state) => <Child {...state} />} />

Pass a function as prop that receives state and returns JSX - use for reusable behavior logic

typescript
<MouseTracker render={({ x, y }) => (
  <span>Position: {x}, {y}</span>
)} />
Children as Function
<Component>{(state) => <Child {...state} />}</Component>

Variation of render props using children prop - cleaner syntax for single render function

typescript
<Toggle>{({ on, toggle }) => (
  <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
)}</Toggle>
Render Props TypeScript
interface Props<T> { data: T; render: (item: T) => ReactNode; }

Type the render function parameter to ensure type safety in consumer components

typescript
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => ReactNode;
}
Slot Pattern
interface Props { header?: ReactNode; footer?: ReactNode; children: ReactNode; }

Named slots for flexible layout composition without deep nesting

typescript
<Card
  header={<h2>Title</h2>}
  footer={<Button>Save</Button>}
>{content}</Card>

Higher-Order Components

Basic HOC
function withFeature<P>(Component: ComponentType<P>): FC<Omit<P, 'feature'>>

Function that takes a component and returns enhanced component - use for cross-cutting concerns

typescript
function withLogger<P>(Wrapped: ComponentType<P>) {
  return (props: P) => { useEffect(() => console.log('mounted')); return <Wrapped {...props} />; };
}
HOC with Config
const withFeature = (config: Config) => <P,>(Component: ComponentType<P>) => FC<P>

Curried HOC that accepts configuration before the component

typescript
const withTheme = (theme: Theme) => <P,>(Comp: ComponentType<P & ThemeProps>) =>
  (props: P) => <Comp {...props} theme={theme} />;
HOC Injected Props
type InjectedProps = { user: User }; type Props<P> = Omit<P, keyof InjectedProps>;

Properly type props that HOC injects to avoid requiring them from consumers

typescript
interface WithUserProps { user: User; }
function withUser<P extends WithUserProps>(Comp: ComponentType<P>) {
  return (props: Omit<P, keyof WithUserProps>) => <Comp {...props as P} user={currentUser} />;
}
HOC Display Name
WrappedComponent.displayName = `withFeature(${Component.displayName || Component.name})`;

Set displayName for better debugging in React DevTools

typescript
const Enhanced = (props: P) => <Wrapped {...props} />;
Enhanced.displayName = `withAuth(${Wrapped.displayName || 'Component'})`;
HOC Forward Ref
const withFeature = <P,>(Comp: ComponentType<P>) => forwardRef<Ref, P>((props, ref) => ...)

Forward refs through HOC to allow parent access to underlying DOM element

typescript
const withTooltip = <P,>(Comp: ComponentType<P>) =>
  forwardRef<HTMLElement, P>((props, ref) => <Comp {...props} ref={ref} />);

Custom Hooks Patterns

Basic Custom Hook
function useCustom<T>(param: T): [T, (val: T) => void]

Extract reusable stateful logic - always prefix with use to enable Rules of Hooks

typescript
function useToggle(initial = false) {
  const [state, setState] = useState(initial);
  const toggle = useCallback(() => setState(s => !s), []);
  return [state, toggle] as const;
}
Hook with Cleanup
useEffect(() => { subscribe(); return () => unsubscribe(); }, [deps]);

Return cleanup function from useEffect to prevent memory leaks

typescript
function useWindowSize() {
  const [size, setSize] = useState({ w: innerWidth, h: innerHeight });
  useEffect(() => { const h = () => setSize({...}); addEventListener('resize', h); return () => removeEventListener('resize', h); }, []);
}
Hook Returning Object
return { value, setValue, reset, isValid } as const;

Return object for named access to multiple values - easier to consume selectively

typescript
function useForm<T>(initial: T) {
  const [values, setValues] = useState(initial);
  const reset = () => setValues(initial);
  return { values, setValues, reset };
}
Generic Data Hook
function useFetch<T>(url: string): { data: T | null; loading: boolean; error: Error | null }

Generic type parameter allows hook to work with any data shape

typescript
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  useEffect(() => { fetch(url).then(r => r.json()).then(setData); }, [url]);
  return data;
}
useReducer Pattern
const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, initialState);

Use for complex state logic with multiple sub-values or next state depends on previous

typescript
type Action = { type: 'inc' } | { type: 'dec' };
const reducer = (state: number, action: Action) =>
  action.type === 'inc' ? state + 1 : state - 1;
Lazy Initialization
const [state, setState] = useState<T>(() => computeExpensiveInitial());

Pass function to useState/useReducer for expensive initial state computation

typescript
const [items, setItems] = useState(() => JSON.parse(localStorage.getItem('items') || '[]'));

Context Pattern

Create Typed Context
const MyContext = createContext<ContextType | null>(null);

Use null default with type union to catch missing provider errors

typescript
interface AuthContext { user: User | null; login: (u: User) => void; }
const AuthCtx = createContext<AuthContext | null>(null);
Context Provider Component
function Provider({ children }: { children: ReactNode }) { return <Ctx.Provider value={val}>{children}</Ctx.Provider>; }

Wrap context provider in custom component to encapsulate state logic

typescript
function AuthProvider({ children }: PropsWithChildren) {
  const [user, setUser] = useState<User | null>(null);
  return <AuthCtx.Provider value={{ user, login: setUser }}>{children}</AuthCtx.Provider>;
}
Safe Context Hook
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

typescript
function useAuth() {
  const ctx = useContext(AuthCtx);
  if (!ctx) throw new Error('useAuth must be within AuthProvider');
  return ctx;
}
Split Context Pattern
const StateCtx = createContext<State>(init); const DispatchCtx = createContext<Dispatch>(noop);

Separate state and dispatch contexts to prevent unnecessary re-renders

typescript
const CountStateCtx = createContext(0);
const CountDispatchCtx = createContext<Dispatch<Action>>(() => {});
Context Selector Pattern
const value = useSyncExternalStore(subscribe, () => selector(getSnapshot()));

Use external store pattern to select partial context and avoid re-renders

typescript
function useContextSelector<T, S>(ctx: Context<T>, selector: (v: T) => S): S {
  const value = useContext(ctx);
  return useMemo(() => selector(value), [value, selector]);
}

Performance Patterns

React.memo
const Memoized = memo<Props>(Component, (prev, next) => prev.id === next.id);

Skip re-render if props unchanged - use for expensive components with stable props

typescript
const ExpensiveList = memo(function List({ items }: { items: Item[] }) {
  return items.map(i => <Row key={i.id} item={i} />);
});
memo with Generic
const MemoGeneric = memo(function <T>(props: Props<T>) { ... }) as <T>(props: Props<T>) => JSX.Element;

Type assertion needed to preserve generic with memo wrapper

typescript
const MemoList = memo(function <T>({ items, render }: ListProps<T>) {
  return <>{items.map(render)}</>;
}) as <T>(props: ListProps<T>) => JSX.Element;
useMemo
const computed = useMemo<T>(() => expensiveCalc(deps), [deps]);

Memoize expensive calculations - only recompute when dependencies change

typescript
const sortedItems = useMemo(() => 
  [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]);
useCallback
const handler = useCallback<(e: Event) => void>((e) => { ... }, [deps]);

Memoize callbacks to maintain referential equality for memo children

typescript
const handleClick = useCallback((id: string) => {
  setSelected(id);
}, []);
Stable Reference Object
const actions = useMemo(() => ({ inc: () => dispatch('inc'), dec: () => dispatch('dec') }), []);

Wrap multiple callbacks in useMemo object to pass single stable reference

typescript
const api = useMemo(() => ({
  save: () => mutate(data),
  reset: () => setData(initial)
}), [data]);

Ref Patterns

forwardRef Basic
const Comp = forwardRef<HTMLInputElement, Props>((props, ref) => <input ref={ref} {...props} />);

Forward ref to child element - use for reusable input components needing imperative focus

typescript
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
  <input ref={ref} className="input" {...props} />
));
useImperativeHandle
useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }), []);

Customize ref value exposed to parent - limit imperative API surface

typescript
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 = ''; } }));
});
Callback Ref
<div ref={(node) => { if (node) measureRef.current = node; }} />

Function ref called with element - use when you need to react to ref attachment

typescript
const [height, setHeight] = useState(0);
const measureRef = useCallback((node: HTMLDivElement | null) => {
  if (node) setHeight(node.getBoundingClientRect().height);
}, []);
Merging Refs
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

typescript
const Button = forwardRef<HTMLButtonElement, Props>((props, extRef) => {
  const intRef = useRef<HTMLButtonElement>(null);
  return <button ref={mergeRefs(intRef, extRef)} {...props} />;
});

Error & Async Patterns

Error Boundary Class
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

typescript
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; }
}
Error Boundary with Reset
componentDidCatch(error: Error, info: ErrorInfo) { logError(error, info); }

Log errors and provide recovery mechanism for users

typescript
<ErrorBoundary
  fallback={({ error, reset }) => (
    <div><p>{error.message}</p><button onClick={reset}>Retry</button></div>
  )}
>{children}</ErrorBoundary>
Suspense Basic
<Suspense fallback={<Loading />}><LazyComponent /></Suspense>

Show fallback while children are loading - use with lazy components or data fetching libs

typescript
const LazyDashboard = lazy(() => import('./Dashboard'));
<Suspense fallback={<Spinner />}><LazyDashboard /></Suspense>
Suspense with ErrorBoundary
<ErrorBoundary fallback={<Error />}><Suspense fallback={<Loading />}><Async /></Suspense></ErrorBoundary>

Combine error and loading states for complete async UI handling

typescript
<ErrorBoundary fallback={<ErrorPage />}>
  <Suspense fallback={<Skeleton />}>
    <UserProfile />
  </Suspense>
</ErrorBoundary>
Nested Suspense
<Suspense fallback={<Shell />}><Header /><Suspense fallback={<ContentSkeleton />}><Content /></Suspense></Suspense>

Nested boundaries allow progressive loading of different UI sections

typescript
<Suspense fallback={<PageSkeleton />}>
  <Nav />
  <Suspense fallback={<ListSkeleton />}><ItemList /></Suspense>
</Suspense>

Portal Pattern

Basic Portal
createPortal(children, document.getElementById('portal-root')!)

Render children into DOM node outside parent hierarchy - use for modals, tooltips, dropdowns

typescript
function Modal({ children }: PropsWithChildren) {
  return createPortal(children, document.getElementById('modal-root')!);
}
Portal with State
const [container] = useState(() => document.createElement('div'));

Create container element in state to ensure stable reference across renders

typescript
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);
}
Conditional Portal
return isOpen ? createPortal(<Modal onClose={close} />, container) : null;

Only create portal when needed to avoid unnecessary DOM nodes

typescript
function Tooltip({ show, children, anchor }: Props) {
  if (!show) return null;
  return createPortal(<div style={getPosition(anchor)}>{children}</div>, document.body);
}
Portal Event Bubbling
<div onClick={handleParentClick}><Portal><button onClick={handlePortalClick} /></Portal></div>

Events still bubble through React tree even though DOM is separate

typescript
// Click in portal still triggers parent onClick in React tree
<div onClick={() => console.log('parent')}>
  <Portal><button>Click</button></Portal>
</div>

Related Content