React in 2026 looks different from React in 2018. Server Components shipped, Suspense matured, the recommended state management consensus moved decisively away from Redux for new apps. Many "best practices" tutorials are out of date. This page is what's actually true now.
A React component is a function from props to UI. Re-running the function with the same props should produce the same output. Side effects (data fetching, subscriptions, timers) live in useEffect and equivalents.
The hard part is managing change: when state updates, components re-render, effects fire, and you have to reason about the order. Most React bugs are about ordering of state updates, effects, and renders.
Internalise: render-as-derivation. UI is a function of state. Don't store derived data in state; compute it during render. The bugs you avoid are huge.
Since React 19 (2024), Server Components are stable. A component runs on the server, generates HTML, never ships its JS to the client. Client components are interactive and ship.
Decisions:
"use client" directive opts a component (and its descendants) into client rendering. Use sparingly.await db.query(...) in the component body. No useEffect, no loading states for the initial render.Frameworks: Next.js (App Router) and Remix are the production options. Vanilla React with Server Components is technically possible but rarely worth it.
Most apps use four hooks 90% of the time.
Local component state. Should be flat (one value per piece of state); arrays of primitives are fine; deeply nested objects often signal a need for refactor.
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
If you're reaching for useReducer, ask first whether you'd be better served by a state manager (Zustand) or by lifting state up to a parent.
Side effects after render. The most-misused hook.
useEffect(() => {
const subscription = fetchData(id);
return () => subscription.cancel();
}, [id]);
Common mistakes:
react-hooks/exhaustive-deps) catches these. Don't disable.useEffect for derived state. Compute during render. useMemo if expensive.useEffect for transformations. Map your props in render. Effects are for side effects (subscriptions, timers, manual DOM, fetching that can't run on the server).Caching. useMemo for values; useCallback for functions.
Both are micro-optimisations. Don't add them prophylactically. Add them when:
React.memo and depends on the value/function.Most components don't need either. Adding them everywhere makes the code worse.
Pass values through the component tree without prop drilling.
Limit context to:
For frequently-changing app-wide data, use a state manager (Zustand) instead of context.
Use the simplest tool that handles your case:
useEffect + useState.Most apps in 2026 use TanStack Query for server state + Zustand (or nothing) for client state. Redux Toolkit is fine if you're already on it; rarely the right choice for new apps.
A component that takes 15 props is a refactor opportunity:
// Bad: many specific props
<Dialog
title="..." onClose={...} hasCloseButton
showFooter footerText="..." footerButtonText="..."
...
/>
// Better: composition
<Dialog onClose={...}>
<DialogTitle>...</DialogTitle>
<DialogBody>...</DialogBody>
<DialogFooter>
<Button>...</Button>
</DialogFooter>
</Dialog>
Children render naturally; the parent doesn't need to know what's inside.
When two components share logic, extract to a custom hook. Custom hooks own state, effects, and computed values; components consume them.
function usePagination(initialPage = 1, pageSize = 20) {
const [page, setPage] = useState(initialPage);
// ...
return { page, setPage, offset: (page - 1) * pageSize, pageSize };
}
Custom hooks compose. A useUserOrders(userId) hook can call useQuery(...) from TanStack Query inside; consumers just call useUserOrders(42).
For non-trivial forms, use a form library: React Hook Form is the production standard. Validation: Zod schemas, integrated.
const schema = z.object({
email: z.string().email(),
age: z.number().int().min(18),
});
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
Saves immense amounts of boilerplate, gets validation right, integrates with TypeScript.
<ErrorBoundary> for catching errors. <Suspense> for handling pending states. With React 19, both are first-class.
Pattern:
<Suspense fallback={<Skeleton />}>
<ErrorBoundary fallback={<ErrorMessage />}>
<UserDetail userId={42} />
</ErrorBoundary>
</Suspense>
This is the modern replacement for if (loading) ... else if (error) ... else ... ladders inside components.
Use it. The marginal cost of TypeScript in a new React project is approximately zero. The marginal benefit grows with codebase size; under-rated by people who haven't done it on a large app.
Conventions:
React.FC<Props> or just regular functions with typed props: Props. Both work; community has settled on regular functions.useState<Type>(initial) when initial is null/undefined and the type can't be inferred.useRef<HTMLInputElement>(null).strict: true in tsconfig.Most React performance work is unnecessary. Profile first.
When it does matter:
react-window or @tanstack/react-virtual for virtualisation. Rendering 10,000 DOM nodes is not free.React.memo and proper memoisation.useDeferredValue, startTransition, or web worker.React.lazy; analyse with @next/bundle-analyzer or equivalent.See WebPerformanceOptimization.
useEffect for derived state. Compute during render.useState for mutable refs. Use useRef. Don't update state when you don't need to re-render.useMemo / useCallback if you've measured a problem.For component testing, React Testing Library + Vitest or Jest. The core idea: test behaviour, not implementation. "When the user clicks this, that text appears" — not "when the state updates, the JSX rerenders with that property."
it('shows error when login fails', async () => {
render(<LoginForm onSubmit={() => Promise.reject('bad creds')} />);
await user.type(screen.getByLabelText(/email/i), 'a@b.c');
await user.type(screen.getByLabelText(/password/i), 'x');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(await screen.findByText(/bad creds/i)).toBeInTheDocument();
});
For E2E tests, Playwright is the consensus pick; Cypress is fine if you're already on it.
For most new React apps in 2026: Next.js with App Router unless there's a specific reason not to.