[ LOG.ENTRY // Jan 1, 2026 ]

React Best Practices : Build Faster, Safer, Cleaner UIs (Without the Usual Pain)

Archive
React Best Practices : Build Faster, Safer, Cleaner UIs (Without the Usual Pain)

Why “best practices” matter (even if you’re already shipping)

React doesn’t punish you immediately for messy code—it waits until your app grows. Then you get:

  • “Why does this rerender 20 times?”
  • “Where does this state even live?”
  • “Why does the UI glitch when I navigate?”
  • “Why is this component impossible to test?”

The fixes are rarely fancy. They’re usually a handful of disciplined habits.

1) Keep components small… but not anxious

A good component does one job:

  • UI component: renders layout, styles, accessibility
  • Container/component: orchestrates data fetching, state, side effects
  • Hook: reusable logic

âś… Pattern: split data + view

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> ); }

2) Put state where it belongs (the “closest common owner” rule)

Before adding global state, try this order:

  • Local component state
  • Lift state up to nearest shared parent
  • Extract a hook used by siblings
  • Context for app-wide concerns (theme/auth/feature flags)
  • External state library (when it’s truly cross-cutting)

âś… Rule of thumb

If fewer than ~3 unrelated areas need the state, it’s probably not “global.”

3) Prefer derived state over duplicated state

Duplicating state creates bugs because it gets out of sync.

❌ Avoid: storing what you can compute

tsx
const [items, setItems] = useState<CartItem[]>([]); const [total, setTotal] = useState(0); // duplicated state (risky)

âś… Do: derive it

tsx
const [items, setItems] = useState<CartItem[]>([]); const total = useMemo( () => items.reduce((sum, i) => sum + i.price * i.qty, 0), [items] );

4) Side effects: keep them predictable

Use effects for syncing with external systems (network, DOM APIs, subscriptions), not for computing values.

âś… UseEffect checklist

  • Put everything you read inside the dependency array (or refactor)
  • Clean up subscriptions/timeouts
  • Avoid effect chains that bounce state back and forth
tsx
useEffect(() => { 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]);

5) Performance: measure first, then optimize the real bottleneck

Most React “performance fixes” are premature. When you do need them, prioritize:

  • React.memo for expensive leaf components
  • useMemo / useCallback to stabilize props only when it helps
  • List virtualization for long lists
  • Avoid rendering work you can defer

âś… Practical React.memo example

tsx
type 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.

6) Data fetching: don’t reinvent caching and retries

If your app fetches data regularly, use a mature query layer (or your framework’s recommended approach). What you want:

  • caching
  • deduping
  • retries
  • background refetch
  • request cancellation
  • stale-while-revalidate patterns

Even if you hand-roll something small, keep a consistent shape:

ts
type AsyncState<T> = | { status: "idle" | "loading"; data?: undefined; error?: undefined } | { status: "success"; data: T; error?: undefined } | { status: "error"; data?: undefined; error: Error };

7) Forms: treat them as a system

Forms aren’t “just inputs.” They have:

  • validation
  • error UI
  • submission states
  • accessibility rules
  • async server errors

âś… Example: accessible field with errors

tsx
type 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> ); }

8) Accessibility: bake it in, don’t “add it later”

Quick wins that prevent real user pain:

  • Use semantic HTML (button, nav, main, label)
  • Don’t remove focus outlines (style them instead)
  • Use aria-* only when semantic HTML can’t express it
  • Use role="alert" for important errors
  • Ensure modals trap focus (use a library if needed)

9) TypeScript: let the compiler do the boring work

Best TS habits in React:

  • Type props explicitly for reusable components
  • Prefer unknown over any
  • Use discriminated unions for UI states
ts
type 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; } }

10) Testing: test behavior, not implementation

Aim for tests that survive refactors:

  • Prefer user-centric tests: “click button → UI updates”
  • Mock network at the boundary (e.g., MSW style approaches)
  • Don’t test internal state directly unless necessary

A good test fails for the right reason.

A simple “Best Practices” checklist you can paste into PRs

  • [ ] Components: one responsibility; logic extracted to hooks when reused
  • [ ] State: minimal, colocated, derived state preferred
  • [ ] Effects: only for external sync; cleaned up properly
  • [ ] Performance: no premature memoization; large lists virtualized
  • [ ] Data: errors + loading states handled; cancel/abort where relevant
  • [ ] Forms: accessible labels; error messages linked; disabled states
  • [ ] A11y: semantic HTML first; keyboard navigation works
  • [ ] Types: no any; good unions for async UI
  • [ ] Tests: behavior-focused; avoids brittle internals

Closing: the “boring” React code is the best React code

If 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.

#react#javascript#cleancode
All Insights