// Inferred const [count, setCount] = useState(0); // Explicit generic const [user, setUser] = useState<User | null>(null); // Object state const [form, setForm] = useState<FormState>({ name: '', email: '' }); // Merge partial updates setForm(prev => ({ ...prev, name: 'Eric' }));
// On mount useEffect(() => { fetchData(); }, []); // On dep change useEffect(() => { fetchUser(userId); }, [userId]); // Cleanup useEffect(() => { const sub = subscribe(); return () => sub.unsubscribe(); }, [dep]);
// DOM ref const inputRef = useRef<HTMLInputElement>(null); return <input ref={inputRef} />; inputRef.current?.focus(); // Mutable value (no re-render) const timerRef = useRef<ReturnType<typeof setTimeout>>(); // Store prev value const prev = useRef<number>(); useEffect(() => { prev.current = value; });
type Action = | { type: 'inc' } | { type: 'set'; payload: number }; function reducer(state: number, action: Action) { switch (action.type) { case 'inc': return state + 1; case 'set': return action.payload; default: return state; } } const [n, dispatch] = useReducer(reducer, 0);
interface ThemeCtx { dark: boolean; } const ThemeContext = createContext<ThemeCtx>( { dark: false } ); // Provider <ThemeContext.Provider value={{ dark }}> {children} </ThemeContext.Provider> // Consumer const { dark } = useContext(ThemeContext);
function useFetch<T>(url: string) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { fetch(url) .then(r => r.json() as T) .then(setData) .catch(setError) .finally(() => setLoading(false)); }, [url]); return { data, loading, error }; }
// Preferred: explicit return type interface Props { name: string; count?: number; // optional onClick: () => void; children?: React.ReactNode; } function MyComp({ name, count = 0, onClick }: Props): JSX.Element { return <div onClick={onClick}>{name}: {count}</div>; } // React.FC (adds children implicitly in older TS) const MyComp: React.FC<Props> = ({ name }) => <div>{name}</div>;
interface ListProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; keyExtractor: (item: T) => string; } function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) { return ( <ul> {items.map(item => ( <li key={keyExtractor(item)}>{renderItem(item)}</li> ))} </ul> ); }
const Input = React.forwardRef< HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement> >((props, ref) => ( <input {...props} ref={ref} /> )); Input.displayName = 'Input'; // useImperativeHandle React.useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }));
// Typed children children: React.ReactNode // most permissive children: React.ReactElement // single element children: JSX.Element // single JSX children: string // text only // Render prop interface P { render: (val: string) => React.ReactNode; } const x = ({ render }: P) => render('hello'); // Children as function children: (val: number) => React.ReactNode;
// Extend HTML element props interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: 'primary' | 'ghost'; } function Button({ variant = 'primary', ...rest }: ButtonProps) { return <button className={variant} {...rest} />; } // Polymorphic 'as' prop (simplified) type AsProp<C extends React.ElementType> = { as?: C; };
class ErrorBoundary extends React.Component< { children: React.ReactNode }, { hasError: boolean } > { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } render() { return this.state.hasError ? <h2>Error!</h2> : this.props.children; } }
| Event | Handler Type |
|---|---|
| onClick | React.MouseEvent<HTMLButtonElement> |
| onChange (input) | React.ChangeEvent<HTMLInputElement> |
| onChange (select) | React.ChangeEvent<HTMLSelectElement> |
| onSubmit | React.FormEvent<HTMLFormElement> |
| onKeyDown | React.KeyboardEvent<HTMLInputElement> |
| onFocus/onBlur | React.FocusEvent<HTMLInputElement> |
| onDrag | React.DragEvent<HTMLDivElement> |
| handler prop | React.MouseEventHandler<HTMLButtonElement> |
// Input change const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setValue(e.target.value); }; // Form submit + prevent default const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); doSomething(); }; // Keyboard shortcut const handleKey = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && e.metaKey) submit(); }; // Get value from event target const val = (e.target as HTMLInputElement).value;
// React wraps native events // Access native: e.nativeEvent const onClick = (e: React.MouseEvent) => { e.stopPropagation(); // synthetic e.preventDefault(); // synthetic e.nativeEvent; // underlying DOM event e.currentTarget; // always the bound element e.target; // element that fired };
// Inline (avoid complex logic) <button onClick={() => setOpen(true)}>Open</button> // Curried handler (passes extra args) const handleClick = (id: string) => (e: React.MouseEvent) => { ... }; <button onClick={handleClick(item.id)}>...</button> // Typed prop handler interface P { onSelect: React.MouseEventHandler<HTMLLIElement>; }
const TabContext = createContext<{ active: string }>({ active: '' }); function Tabs({ children, defaultTab }: TabsProps) { const [active, setActive] = useState(defaultTab); return <TabContext.Provider value={{ active }}>{children}</...>; } Tabs.Tab = function Tab({ id, children }: ...) { const { active } = useContext(TabContext); return <div aria-selected={active === id}>{children}</div>; }; // Usage: <Tabs><Tabs.Tab id="a">A</Tabs.Tab></Tabs>
function withAuth<P extends object>( Component: React.ComponentType<P> ) { return function WithAuth(props: P) { const { isAuthed } = useAuth(); if (!isAuthed) return <Redirect to="/login" />; return <Component {...props} />; }; } const ProtectedPage = withAuth(Dashboard); // ProtectedPage.displayName = 'withAuth(Dashboard)'
// Controlled (React owns value) const [val, setVal] = useState(''); <input value={val} onChange={e => setVal(e.target.value)} /> // Uncontrolled (DOM owns value) const ref = useRef<HTMLInputElement>(null); <input defaultValue="initial" ref={ref} />; ref.current?.value; // read on demand // Controlled component pattern interface P { value: string; onChange(v: string): void; }
// Parent owns shared state function Parent() { const [shared, setShared] = useState(''); return ( <> <ChildA value={shared} onChange={setShared} /> <ChildB value={shared} /> </> ); } // Both children stay in sync // ChildA writes, ChildB reads
// Portal (render outside DOM hierarchy) import { createPortal } from 'react-dom'; createPortal(<Modal />, document.getElementById('modal-root')!); // Suspense + lazy const Dashboard = React.lazy(() => import('./Dashboard')); <Suspense fallback={<Spinner />}> <Dashboard /> </Suspense>
type Status = 'idle' | 'loading' | 'success' | 'error'; const [status, setStatus] = useState<Status>('idle'); // Discriminated union state type State = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: Data } | { status: 'error'; error: Error }; // Narrows automatically in if/switch
| Type | Effect | Example |
|---|---|---|
Partial<T> | All props optional | Partial<User> |
Required<T> | All props required | Required<Config> |
Readonly<T> | Immutable | Readonly<State> |
Pick<T, K> | Keep keys K | Pick<User, 'id'|'name'> |
Omit<T, K> | Remove keys K | Omit<User, 'password'> |
Record<K,V> | Map type | Record<string, number> |
ReturnType<T> | Fn return type | ReturnType<typeof fn> |
Parameters<T> | Fn param tuple | Parameters<typeof fn> |
NonNullable<T> | Remove null/undef | NonNullable<string|null> |
Extract<T,U> | Intersection | Extract<A|B, B|C> |
Exclude<T,U> | Difference | Exclude<A|B, B> |
| Type | Use for |
|---|---|
React.ReactNode | Any renderable (string, elem, null…) |
React.ReactElement | JSX element specifically |
React.FC<P> | Function component (adds children) |
React.ComponentType<P> | FC or class, useful for HOCs |
React.ElementType | String or component ('div', Button) |
React.CSSProperties | style={{ }} props |
React.Dispatch<A> | useReducer dispatch |
React.SetStateAction<T> | useState setter arg |
React.Ref<T> | ref prop |
React.MutableRefObject<T> | useRef (non-null) |
// Discriminated union (narrows w/ switch) type Result<T> = { ok: true; data: T } | { ok: false; error: string }; // Template literal types type EventName = `on${Capitalize<string>}`; // Conditional types type IsArray<T> = T extends any[] ? true : false; // Mapped types type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; // Infer keyword type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// Skip re-render if props unchanged const ExpensiveChild = React.memo( function Child({ data }: { data: Data }) { return <div>{data.value}</div>; } ); // Custom comparator React.memo(Child, (prev, next) => prev.id === next.id ); // âš Only helps if parent re-renders often // âš Inline objects/fns bypass memo!
// Stable fn reference across renders const handleClick = useCallback( (id: string) => { deleteItem(id); }, [deleteItem] // deps ); // Pair with React.memo to prevent child re-render const fetchPage = useCallback( async (page: number) => { const data = await api(page); setItems(data); }, [] // api is stable );
// Memoize expensive computation const sorted = useMemo( () => [...items].sort((a, b) => a.name.localeCompare(b.name)), [items] ); // Stable object reference (stops child re-render) const config = useMemo( () => ({ timeout: 3000, retries }), [retries] ); // ⚠Don't overuse — has its own cost // Use when: expensive calc OR stable ref needed
// Key tells React element identity items.map(item => <Row key={item.id} />); // ✅ items.map((item, i) => <Row key={i} />); // ⚠avoid // Force re-mount with key change <Form key={userId} /> // new key = fresh mount // Fragment shorthand <>...</> // no DOM node <React.Fragment key={id}>...</React.Fragment> // keyed
// React 18: automatic batching everywhere setTimeout(() => { setA(1); setB(2); // single re-render in React 18 }, 0); // Opt-out of batching import { flushSync } from 'react-dom'; flushSync(() => setState(x)); // useTransition: mark low-priority update const [isPending, startTransition] = useTransition(); startTransition(() => setFilter(val)); // deferrable
| Pitfall | Fix |
|---|---|
| Stale closure in effect | Add dep or use ref |
| Infinite effect loop | Check deps, stable refs |
| New obj/arr each render | useMemo or move outside |
| Missing cleanup | Return fn from useEffect |
| State mutation | Always return new object/array |
| Index as key | Use stable ID |
| Over-render with context | Split contexts or memoize |