// LOADING
// LOADING
// LOADING_ARTICLE
React doesn’t punish you immediately for messy code—it waits until your app grows. Then you get:
The fixes are rarely fancy. They’re usually a handful of disciplined habits.
A good component does one job:
tsx// UsersPage.tsx (container) import { useUsers } from "./useUsers"; import { UsersTable } from "./UsersTable"; export function UsersPage() { const { users, isLoading, error, refetch } = useUsers(); if (isLoading) return <p>Loading users…</p>; if (error) return <ErrorState message={error.message} onRetry={refetch} />; return <UsersTable users={users} />; } function ErrorState({ message, onRetry }: { message: string; onRetry: () => void }) { return ( <div role="alert"> <p>{message}</p> <button onClick={onRetry}>Try again</button> </div> ); }
tsx// UsersTable.tsx (pure view) type User = { id: string; name: string; email: string }; export function UsersTable({ users }: { users: User[] }) { return ( <table> <thead> <tr><th>Name</th><th>Email</th></tr> </thead> <tbody> {users.map(u => ( <tr key={u.id}> <td>{u.name}</td> <td>{u.email}</td> </tr> ))} </tbody> </table> ); }
Before adding global state, try this order:
If fewer than ~3 unrelated areas need the state, it’s probably not “global.”
Duplicating state creates bugs because it gets out of sync.
tsxconst [items, setItems] = useState<CartItem[]>([]); const [total, setTotal] = useState(0); // duplicated state (risky)
tsxconst [items, setItems] = useState<CartItem[]>([]); const total = useMemo( () => items.reduce((sum, i) => sum + i.price * i.qty, 0), [items] );
Use effects for syncing with external systems (network, DOM APIs, subscriptions), not for computing values.
tsxuseEffect(() => { const controller = new AbortController(); fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal }) .then(r => r.json()) .then(setResults) .catch(err => { if (err.name !== "AbortError") setError(err); }); return () => controller.abort(); }, [query]);
Most React “performance fixes” are premature. When you do need them, prioritize:
React.memo for expensive leaf componentsuseMemo / useCallback to stabilize props only when it helpsReact.memo exampletsxtype RowProps = { name: string; onSelect: (name: string) => void }; const Row = React.memo(function Row({ name, onSelect }: RowProps) { return <button onClick={() => onSelect(name)}>{name}</button>; }); export function Rows({ names }: { names: string[] }) { const onSelect = useCallback((name: string) => { console.log("selected", name); }, []); return names.map(n => <Row key={n} name={n} onSelect={onSelect} />); }
Tip: If you don’t have a measurable slowdown, skip memoization. It adds complexity.
If your app fetches data regularly, use a mature query layer (or your framework’s recommended approach). What you want:
Even if you hand-roll something small, keep a consistent shape:
tstype AsyncState<T> = | { status: "idle" | "loading"; data?: undefined; error?: undefined } | { status: "success"; data: T; error?: undefined } | { status: "error"; data?: undefined; error: Error };
Forms aren’t “just inputs.” They have:
tsxtype FieldProps = { id: string; label: string; error?: string; } & React.InputHTMLAttributes<HTMLInputElement>; export function TextField({ id, label, error, ...props }: FieldProps) { const errorId = `${id}-error`; return ( <div> <label htmlFor={id}>{label}</label> <input id={id} aria-invalid={Boolean(error)} aria-describedby={error ? errorId : undefined} {...props} /> {error && ( <p id={errorId} role="alert"> {error} </p> )} </div> ); }
Quick wins that prevent real user pain:
button, nav, main, label)aria-* only when semantic HTML can’t express itrole="alert" for important errorsBest TS habits in React:
unknown over anytstype Loadable<T> = | { state: "loading" } | { state: "ready"; data: T } | { state: "error"; error: string }; function renderUser(state: Loadable<{ name: string }>) { switch (state.state) { case "loading": return "Loading…"; case "ready": return state.data.name; case "error": return state.error; } }
Aim for tests that survive refactors:
A good test fails for the right reason.
any; good unions for async UIIf your components are predictable, your state is minimal, and your side effects are contained, React becomes almost relaxing. And when the app scales, you won’t be untangling a rerender mystery at 2 a.m.