react
React is a JavaScript library for building user interfaces out of components. You describe what each state of the UI looks like, and React updates the DOM when your data changes. Components hold their own state and compose into bigger UIs, logic lives in JavaScript rather than templates, and the same model renders in the browser, on the server via Node, and on mobile through React Native. Note that the react package itself is only the component runtime; the actual DOM rendering lives in react-dom.
The default choice for interactive UIs, and the default is worth something: docs, hiring, and third-party libraries all point here. Just remember React is a rendering library, not a framework, so choosing the rest of the stack is still your job.
Use it if
- You are building an app with lots of interactive, stateful UI (dashboards, editors, feeds) rather than mostly-static pages
- You want the largest hiring pool, component ecosystem, and answered-questions surface in frontend
- You plan to share skills or code with React Native mobile apps
- You need SSR or React Server Components, which you get through a framework such as Next.js or React Router framework mode
- Your site is mostly static content; you would ship and hydrate a client runtime for pages that never needed one
- You want one framework that decides routing, data fetching, and state for you; React alone decides none of that, so the surrounding stack is a series of choices you have to make and maintain
- You are payload-sensitive for small widgets; the real weight is react-dom, many times larger than the 2.8 kB react package, while preact offers the same component model in a fraction of the size
- Your team resents churn; hooks, concurrent rendering, and Server Components each reshaped idiomatic React, and long-lived codebases end up carrying all three eras at once
Setup reality
The react package renders nothing by itself: you also install react-dom at a matching version, and JSX needs a build step, so in practice everyone starts from Vite or a framework rather than wiring the transform by hand. StrictMode double-invokes renders and effects in development, which surprises newcomers with duplicate fetches. React itself upgrades cleanly; the pain lives in the ecosystem, where peer dependencies pinned to older React majors block upgrades.
Patterns
Mount an app into the DOMrender-root
import { createRoot } from 'react-dom/client';
import App from './App';
const root = createRoot(document.getElementById('root'));
root.render(<App />);createRoot replaced ReactDOM.render in v18; create the root once, not on every render call.
Hold and update component statelocal-state
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Clicked {count} times
</button>
);
}Use the updater-function form when the next value depends on the previous one; state updates inside one event are batched.
Fetch data in an effect with cleanupfetch-in-effect
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
});
return () => ctrl.abort();
}, [id]);StrictMode mounts components twice in development; without the abort cleanup you get duplicate requests and setState-after-unmount noise.
Read a promise with use() under Suspensesuspense-data
import { use, Suspense } from 'react';
function Profile({ userPromise }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
<Suspense fallback={<Spinner />}>
<Profile userPromise={userPromise} />
</Suspense>;Create the promise outside render (in a parent, loader, or cache); a promise created during render refetches on every render.
Submit a form with useActionStateform-action
import { useActionState } from 'react';
function Rename() {
const [error, submitAction, isPending] = useActionState(
async (prev, formData) => {
const res = await save(formData.get('name'));
return res.ok ? null : res.message;
},
null
);
return (
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>Save</button>
{error && <p>{error}</p>}
</form>
);
}New in React 19; the action receives (previousState, formData) and pending state comes for free.
Show optimistic UI while a mutation runsoptimistic-update
import { useOptimistic } from 'react';
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function formAction(formData) {
addOptimistic({ text: formData.get('text') });
await createTodo(formData);
}addOptimistic must run inside an action or transition, otherwise the optimistic state reverts immediately.
Share a value without prop drillingcontext-share
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext value="dark">
<Toolbar />
</ThemeContext>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={theme} />;
}React 19 lets you render <Context> directly as the provider; every consumer re-renders when the value changes, so memoize object values.
Pass a ref to focus a child inputref-dom-access
import { useRef } from 'react';
function TextInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
function Form() {
const inputRef = useRef(null);
return (
<>
<TextInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}React 19 passes ref as a normal prop to function components; forwardRef is no longer needed for new code.
Skip recomputing and re-renderingmemoize-expensive
import { memo, useMemo } from 'react';
const total = useMemo(
() => items.reduce((sum, i) => sum + i.price, 0),
[items]
);
const Row = memo(function Row({ item }) {
return <li>{item.name}</li>;
});memo only helps when props stay referentially stable; an inline object or arrow prop defeats it on every render.
Lazy-load a componentcode-split
import { lazy, Suspense } from 'react';
const Settings = lazy(() => import('./Settings'));
<Suspense fallback={<Spinner />}>
<Settings />
</Suspense>;lazy only understands default exports; re-export a named component as default to split it.
Render a list with stable keyslist-keys
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>Array index as key breaks component state when items are inserted, removed, or reordered; use a stable id.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| preact | npm | You need the React component model in a few kilobytes for widgets and embeds |
| vue | npm | You want a progressive framework with official router and state libraries instead of assembling your own stack |
| svelte | npm | You prefer compile-time components that ship less runtime JavaScript to the client |
| solid-js | npm | You want JSX with fine-grained reactivity and no virtual DOM re-render model |