Most React tutorials are long. Most cheat sheets are outdated. This one is neither.
It is written for React 19.2 (the current docs at react.dev), and it works for three kinds of people:
- Beginners and intermediates: read it top to bottom like a crash course.
- Job seekers: jump to the Interview Rapid Fire and Gotchas sections.
- Working devs: use the table of contents as a quick revision index.
Every concept follows the same pattern: what it is, the smallest useful example, and the trap to avoid.
Table of Contents
- JavaScript you need first
- Setup and project structure
- How React thinks
- JSX rules
- Components and props
- Conditional rendering and lists
- Events
- State
- All built-in hooks at a glance
- useEffect
- useRef
- useReducer and Context
- Performance
- Suspense and Transitions
- Forms and Actions (React 19)
- What is new in React 19 and 19.2
- Server Components
- Custom hooks
- Portals, Fragments, Error Boundaries
- Styling
- Routing
- Data fetching and state management
- TypeScript with React
- Testing
- Accessibility and security
- Gotchas that bite everyone
- Interview Rapid Fire
- Legacy stuff you may still see
- Official resources
1. JavaScript you need first
React is 80% JavaScript. If these are shaky, fix them first (MDN JavaScript guide).
| Concept | Example |
|---|---|
| Destructuring |
const { name, age } = user; const [a, b] = arr;
|
| Spread |
{ ...user, age: 30 } [...items, newItem]
|
| Array methods |
map, filter, find, reduce, some, every
|
| Optional chaining | user?.address?.city |
| Nullish coalescing |
value ?? 'default' (only null or undefined) |
| Template literals | `Hello ${name}` |
| ES modules |
export default X / import X from './X' / export { a, b }
|
| Async |
async/await, Promise.all, fetch
|
| Closures | A function remembers the variables around it. Explains stale state bugs. |
2. Setup and project structure
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
Use --template react-ts for TypeScript.
| Tool | Use when |
|---|---|
| Vite | Client-side SPA, dashboards, learning |
| Next.js | Full-stack, SEO, server rendering, Server Components |
| React Router (framework mode) | Full-stack with React Router's data APIs |
| Expo | React Native mobile apps |
Create React App is deprecated. The React team officially sunset it in early 2025. Do not start new projects with it.
Entry point:
// main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
Typical structure:
src/
main.jsx entry point
App.jsx root component
components/ reusable UI (Button, Modal)
features/ feature folders (auth, cart)
hooks/ custom hooks (useDebounce)
pages/ or routes/ route-level components
lib/ or utils/ helpers, API clients
Naming rules: components are PascalCase, hooks start with use, one component per file is a good default.
3. How React thinks
UI = function of state. You describe what the screen should look like for the current data. React figures out the DOM changes.
Every update has 3 steps:
- Trigger: initial render, or a state update.
- Render: React calls your component functions to compute the new JSX.
- Commit: React updates the real DOM only where something changed.
A component re-renders when:
- its own state changes
- its parent re-renders (children re-render too, by default)
- a context it reads changes
Rules of React (non-negotiable):
- Components and hooks must be pure: same inputs, same output.
- Never mutate props, state, or variables created before render.
- Side effects go in event handlers or effects, never in the render body.
- Only call hooks at the top level of components or custom hooks. Not inside
if, loops, or nested functions.
Re-render does not mean the DOM is rebuilt. It means your function ran again.
4. JSX rules
JSX is syntax that compiles to JavaScript function calls. It is not HTML.
| HTML | JSX |
|---|---|
class |
className |
for |
htmlFor |
onclick |
onClick (camelCase events) |
style="color:red" |
style={{ color: 'red' }} (object, camelCase props) |
<input> |
<input /> (every tag must close) |
<!-- comment --> |
{/* comment */} |
function Card({ title, price }) {
const tax = price * 0.18;
return (
<>
<h2 className="title">{title}</h2>
<p>Total: {(price + tax).toFixed(2)}</p>
<img src="/a.png" alt="Product photo" />
</>
);
}
-
{}accepts any expression, not statements (noif, nofor). - A component returns one root. Use
<>...</>(Fragment) to avoid extra divs. - JSX escapes strings by default, which protects you from XSS.
dangerouslySetInnerHTMLturns that protection off.
5. Components and props
A component is a function that takes props and returns JSX.
function Button({ label, variant = 'primary', onClick, children }) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{label}
{children}
</button>
);
}
<Button label="Save" variant="danger" onClick={handleSave} />
- Props are read-only. Never write
props.x = 1. - Set defaults with destructuring defaults (
variant = 'primary').defaultPropson function components was removed in React 19. -
childrenis whatever you put between the tags. This is composition, React's answer to inheritance. - Spread to forward many props:
<input {...rest} />.
Composition example:
function Card({ children }) {
return <div className="card">{children}</div>;
}
<Card>
<h2>Hello</h2>
<p>Any JSX works here.</p>
</Card>
Pass data down with props. Pass events up with function props. That is the entire data flow.
6. Conditional rendering and lists
// ternary: either or
{isLoggedIn ? <Dashboard /> : <Login />}
// && : show or nothing
{hasError && <ErrorMessage />}
// early return
if (!user) return <Spinner />;
Trap:
{count && <Badge />}renders a literal0whencountis 0. Use{count > 0 && <Badge />}.
Lists:
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
Key rules:
- Must be unique among siblings and stable (same item, same key, always).
- Use a real ID from your data. Avoid
Math.random(). - Array index is only safe for lists that never reorder, filter, or insert.
- Key is not a prop. The child cannot read it.
- Bad keys cause wrong state to stick to wrong items, not just slow renders.
7. Events
function Demo() {
function handleClick(e) {
e.preventDefault(); // stop default browser behavior
e.stopPropagation(); // stop bubbling
}
return (
<>
<button onClick={handleClick}>Pass the function</button>
<button onClick={() => remove(id)}>Pass an arrow to send arguments</button>
</>
);
}
- Pass the function, do not call it.
onClick={fn}is right.onClick={fn()}runs during render. - Event names are camelCase:
onChange,onSubmit,onKeyDown,onMouseEnter. - React's
onChangefires on every keystroke, unlike the DOMchangeevent. - Events bubble. Use the
Capturesuffix (onClickCapture) for the capture phase.
8. State
State is memory that triggers a re-render when it changes.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
State is a snapshot
Inside one render, count never changes. setCount schedules the next render.
setCount(count + 1);
setCount(count + 1);
setCount(count + 1); // result: +1 (all three used the same old count)
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1); // result: +3 (updater function gets the latest)
Rule: if the new value depends on the old one, use the updater form.
More state facts
- Updates are batched automatically (events, timeouts, promises, everywhere since React 18).
- Lazy initial value runs only once:
useState(() => readFromStorage()). - Setting the same value (compared with
Object.is) skips the re-render. - State is per component instance. Two
<Counter />have separate counts.
Never mutate. Always replace.
| Task | Avoid (mutates) | Use (returns new) |
|---|---|---|
| Add to array |
push, unshift
|
[...arr, item] |
| Remove |
splice, pop
|
arr.filter(x => x.id !== id) |
| Replace | arr[i] = x |
arr.map(x => x.id === id ? { ...x, done: true } : x) |
| Sort / reverse |
sort(), reverse()
|
arr.toSorted(), arr.toReversed() or [...arr].sort()
|
| Update object | obj.a = 1 |
{ ...obj, a: 1 } |
| Nested object | user.address.city = 'X' |
{ ...user, address: { ...user.address, city: 'X' } } |
For deep nesting, use Immer (useImmer) or flatten your state.
Choosing state structure (from react.dev)
- Group values that change together into one object.
-
Avoid contradictions (
isLoading+isError+isSuccessbecomes onestatusstring). - Avoid redundant state. If you can calculate it from other state or props, calculate it during render.
-
Avoid duplication. Store an
id, not a copy of the whole object. - Avoid deep nesting.
// derive, do not sync
const [items, setItems] = useState([]);
const total = items.reduce((sum, i) => sum + i.price, 0); // no useState, no useEffect
Lifting state up
Two components need the same data? Move the state to their closest common parent and pass it down with props.
Reset state with key
<Profile key={userId} userId={userId} /> // new userId means fresh state
9. All built-in hooks at a glance
| Hook | One-line purpose |
|---|---|
useState |
Local state |
useReducer |
Local state with complex update logic |
useContext |
Read context (skip prop drilling) |
useRef |
Mutable box that does not trigger renders, or a DOM handle |
useImperativeHandle |
Customize what a parent gets through a ref |
useEffect |
Sync with an external system after paint |
useLayoutEffect |
Same, but runs before paint (measure layout) |
useInsertionEffect |
For CSS-in-JS library authors |
useEffectEvent |
Non-reactive logic inside an effect (19.2) |
useMemo |
Cache a calculated value |
useCallback |
Cache a function |
useTransition |
Mark a state update as non-urgent |
useDeferredValue |
Lag a value behind for smooth UI |
useId |
Stable unique ID for accessibility attributes |
useSyncExternalStore |
Subscribe to a non-React store safely |
useDebugValue |
Label for custom hooks in DevTools |
useActionState |
State plus pending flag for form actions (19) |
useOptimistic |
Show the expected result before the server answers (19) |
useFormStatus |
Read parent form's pending status (from react-dom) |
use |
Read a promise or context during render (19, an API, not a classic hook) |
Rules of Hooks: top level only, only in components or custom hooks, same order every render. Install eslint-plugin-react-hooks and let it yell at you.
10. useEffect
Effects are for synchronizing with things outside React: network, timers, browser APIs, third-party widgets.
useEffect(() => {
// setup: runs after render
const id = setInterval(tick, 1000);
return () => {
// cleanup: runs before the next effect and on unmount
clearInterval(id);
};
}, [dependencies]);
| Dependency array | Runs |
|---|---|
| none | After every render |
[] |
After first render only |
[a, b] |
After first render and whenever a or b changed |
Dependencies are not a choice. Every reactive value used inside (props, state, variables from the component) must be listed. The linter enforces this.
Fetching data (the safe pattern)
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setUser(await res.json());
} catch (err) {
if (err.name !== 'AbortError') setError(err);
}
}
load();
return () => controller.abort(); // prevents race conditions
}, [id]);
fetchdoes not reject on 404 or 500. Always checkres.ok.
In real apps, prefer TanStack Query or your framework's data loading over hand-written fetch effects.
Strict Mode
In development, <StrictMode> runs setup, cleanup, setup again on mount. If this breaks your app, your cleanup is missing. It is a feature, not a bug. Production runs once.
You might not need an effect
| You wrote an effect to... | Do this instead |
|---|---|
| Compute data from props or state | Calculate during render |
| Reset state when a prop changes | Use key
|
| Handle a button click | Put the logic in the event handler |
| Send analytics for a user action | Event handler |
| Chain state updates | Compute everything in one handler |
useEffectEvent (React 19.2)
Use it when an effect needs the latest props or state but should not re-run because of them.
import { useEffect, useEffectEvent } from 'react';
function Chat({ roomId, theme }) {
const onConnected = useEffectEvent(() => showToast('Connected', theme));
useEffect(() => {
const conn = createConnection(roomId);
conn.on('connected', () => onConnected());
conn.connect();
return () => conn.disconnect();
}, [roomId]); // theme is not a dependency, by design
}
Call effect events only from inside effects. Do not pass them to children or put them in dependency arrays.
useLayoutEffect
Runs after DOM changes but before the browser paints. Use it to measure an element (tooltip position) and adjust without flicker. It blocks painting, so use it rarely.
11. useRef
A ref is a box with a .current property. Changing it does not re-render.
function SearchBox() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>
);
}
Use useRef for |
Use useState for |
|---|---|
| DOM nodes (focus, scroll, measure) | Anything shown on screen |
| Timer IDs, previous values | Anything that must trigger a re-render |
| Mutable values the UI does not display | Form values you want to control |
Do not read or write
ref.currentduring render (except one-time initialization). Do it in handlers and effects.
Ref as a prop (React 19): function components now receive ref as a normal prop. forwardRef is no longer needed for new code.
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
Ref callbacks can return a cleanup function (19):
<div ref={node => {
const observer = new ResizeObserver(handler);
observer.observe(node);
return () => observer.disconnect();
}} />
12. useReducer and Context
useReducer
Use it when state updates are many, related, or complex.
function reducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: crypto.randomUUID(), text: action.text, done: false }];
case 'toggle':
return state.map(t => (t.id === action.id ? { ...t, done: !t.done } : t));
case 'remove':
return state.filter(t => t.id !== action.id);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
const [todos, dispatch] = useReducer(reducer, []);
dispatch({ type: 'add', text: 'Learn React' });
Reducers must be pure: no fetch, no random, no mutation.
useState |
useReducer |
|---|---|
| Simple, independent values | Many related fields, many action types |
| Less code | Logic is centralized and testable |
Context
Context lets any descendant read a value without passing props through every level.
import { createContext, useContext, useState, useMemo } from 'react';
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
// React 19: use the context itself as the provider
// React 18 and older: <ThemeContext.Provider value={value}>
<ThemeContext value={value}>
<Page />
</ThemeContext>
);
}
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return <button onClick={() => setTheme(t => (t === 'light' ? 'dark' : 'light'))}>{theme}</button>;
}
Context facts:
- When the value changes, every consumer re-renders. Keep values stable (
useMemo) and split big contexts (one for state, one for dispatch, or one per concern). - Great for: theme, current user, language, router, small shared UI state.
- Not great for: fast-changing data (mouse position, big lists). Use a store like Zustand or Redux Toolkit.
-
use(ThemeContext)also works and, unlikeuseContext, can be called insideifblocks.
13. Performance
Measure first. Open React DevTools Profiler. Do not guess.
Order of fixes (cheapest first):
- Move state down. Put state in the smallest component that needs it.
-
Lift content up with
children. A component receivingchildrenas a prop does not re-render its children when its own state changes. - Stable keys and no expensive work inside render.
-
Virtualize long lists (
@tanstack/react-virtual,react-window). -
Code split with
lazyandSuspense. - Memoize (below).
Memoization tools
// skip re-render if props are shallow-equal
const Row = memo(function Row({ item }) { /* ... */ });
// cache an expensive calculation
const sorted = useMemo(() => bigList.toSorted(compare), [bigList]);
// cache a function so memo'd children see the same reference
const onSelect = useCallback(id => setSelected(id), []);
| Tool | Caches | Why it matters |
|---|---|---|
memo(Component) |
The component's render result | Skips re-render when props are equal |
useMemo |
A value | Avoids heavy recalculation, keeps object identity stable |
useCallback |
A function | Keeps function identity stable for memo or effect deps |
useCallbackanduseMemoare useless on their own. They help only when the result goes to amemochild, a dependency array, or an expensive calculation.
React Compiler (v1.0)
The React Compiler is a build-time tool that adds memoization automatically. With it, you write less manual useMemo, useCallback and memo. It is opt-in through a Babel plugin. Check the docs for your build tool.
14. Suspense and Transitions
Suspense
Shows a fallback while something is loading (lazy code, or data from Suspense-enabled sources).
import { lazy, Suspense } from 'react';
const Chart = lazy(() => import('./Chart'));
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
Transitions
Mark updates as non-urgent so typing and clicking stay responsive.
const [isPending, startTransition] = useTransition();
function selectTab(tab) {
startTransition(() => setTab(tab)); // UI stays interactive while the new tab renders
}
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // lags behind while typing
return <Results query={deferredQuery} />;
| Hook | Use when |
|---|---|
useTransition |
You own the state update and can wrap it |
useDeferredValue |
You receive a value and want to defer work based on it |
15. Forms and Actions (React 19)
Controlled input (React owns the value)
const [name, setName] = useState('');
<input value={name} onChange={e => setName(e.target.value)} />
Uncontrolled input (the DOM owns the value)
<form onSubmit={e => {
e.preventDefault();
const data = new FormData(e.currentTarget);
console.log(data.get('email'));
}}>
<input name="email" type="email" />
<button>Send</button>
</form>
| Controlled | Uncontrolled |
|---|---|
| Live validation, formatting, dependent fields | Simple forms, file inputs, fewer re-renders |
Form Actions
In React 19, <form action={fn}> accepts a function. It receives FormData, runs as a transition, and uncontrolled fields reset after success.
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
async function subscribe(prevState, formData) {
const res = await fetch('/api/subscribe', { method: 'POST', body: formData });
if (!res.ok) return { error: 'Something went wrong. Try again.' };
return { error: null };
}
function SubmitButton() {
const { pending } = useFormStatus(); // must be rendered INSIDE the form
return <button disabled={pending}>{pending ? 'Saving...' : 'Subscribe'}</button>;
}
function SubscribeForm() {
const [state, formAction] = useActionState(subscribe, { error: null });
return (
<form action={formAction}>
<input name="email" type="email" required />
<SubmitButton />
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
useOptimistic
Show the result instantly. React rolls it back automatically if the action fails.
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(current, newTitle) => [...current, { id: 'temp', title: newTitle, sending: true }]
);
async function addTodo(formData) {
const title = formData.get('title');
addOptimistic(title);
await saveTodo(title); // then update the real state
}
<form action={addTodo}>...</form>
Validation tips
- Frontend validation is for UX only. Always validate again on the server.
- For big forms, use a library: React Hook Form, TanStack Form, or Formik.
- Use schema validation (Zod, Valibot) and share the schema between client and server.
16. What is new in React 19 and 19.2
| Feature | What changed |
|---|---|
| Actions | Async functions in transitions. Pending, errors and optimistic updates handled for you. |
useActionState, useFormStatus, useOptimistic
|
Form and mutation helpers (section 15). |
use API |
Read a promise or context during render, can be conditional. |
ref as a prop |
No more forwardRef for function components. |
<Context> as provider |
Replaces <Context.Provider>. |
| Document metadata |
<title>, <meta>, <link> rendered inside components are hoisted to <head>. |
| Stylesheet and resource support | Better preloading with preload, preinit from react-dom. |
| Better error reporting | Less duplicated errors, new onCaughtError and onUncaughtError root options. |
| Custom Elements support | Web Components work properly. |
<Activity> (19.2) |
Hide parts of the UI but keep their state. |
useEffectEvent (19.2) |
Non-reactive logic inside effects. |
| Performance Tracks (19.2) | React data in the Chrome DevTools performance timeline. |
The use API:
import { use, Suspense } from 'react';
function Comments({ commentsPromise }) {
const comments = use(commentsPromise); // suspends until resolved
return comments.map(c => <p key={c.id}>{c.text}</p>);
}
<Suspense fallback={<p>Loading...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
Create the promise outside the client component render (in a Server Component, a loader, or a cache). Creating a new promise on every render causes an infinite loop.
<Activity>:
import { Activity } from 'react';
<Activity mode={tab === 'inbox' ? 'visible' : 'hidden'}>
<Inbox /> {/* state kept, effects cleaned up while hidden */}
</Activity>
Removed in 19 (common in old tutorials): ReactDOM.render (use createRoot), ReactDOM.hydrate (use hydrateRoot), propTypes and defaultProps on function components, string refs, legacy context, findDOMNode.
17. Server Components
React Server Components (RSC) run on the server (at build time or per request). They need a framework that supports them, such as Next.js (App Router).
| Server Component | Client Component | |
|---|---|---|
| Default in Next.js App Router | Yes | No, needs 'use client'
|
Can be async and read DB or files directly |
Yes | No |
Can use useState, useEffect, events |
No | Yes |
| JS sent to the browser | None | Yes |
// Server Component (no directive needed in frameworks like Next.js)
async function Products() {
const products = await db.products.findMany();
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
'use client'; // top of file: this file and its imports run in the browser
import { useState } from 'react';
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? 'Liked' : 'Like'}</button>;
}
Server Functions (formerly Server Actions) use 'use server'. They are server-side functions you call from the client, often as form actions.
'use server';
export async function createPost(formData) {
await db.posts.create({ title: formData.get('title') });
}
Remember:
-
'use client'marks a boundary. Everything imported below it becomes client code. - Push
'use client'as deep (as small) as possible. - Props passed from server to client must be serializable (no functions, except Server Functions).
- Treat every Server Function as a public HTTP endpoint: authenticate, authorize and validate inputs.
- A critical RSC vulnerability was disclosed and patched in the React blog's security notice. Keep React and your framework updated.
18. Custom hooks
A custom hook is a function whose name starts with use and that calls other hooks. It shares logic, not state. Each call gets its own state.
function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
const debouncedSearch = useDebounce(search, 500);
Good candidates: useDebounce, useLocalStorage, useMediaQuery, useOnClickOutside, useFetch (or just use TanStack Query).
For browser-store subscriptions (online status, window size), use useSyncExternalStore:
function useOnlineStatus() {
return useSyncExternalStore(
callback => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
},
() => navigator.onLine,
() => true // server snapshot
);
}
19. Portals, Fragments, Error Boundaries
Fragments
<>
<Header />
<Main />
</>
// when you need a key (in lists), import it
<Fragment key={item.id}>...</Fragment>
Portals
Render children into a different DOM node. Used for modals, tooltips, dropdowns that must escape overflow: hidden or z-index traps.
import { createPortal } from 'react-dom';
function Modal({ children }) {
return createPortal(<div className="modal">{children}</div>, document.body);
}
Events still bubble through the React tree, not the DOM tree.
Error Boundaries
Catch errors thrown while rendering children and show a fallback. They still must be class components (or use the react-error-boundary library).
import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logError(error, info.componentStack);
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
<ErrorBoundary fallback={<p>Something broke.</p>}>
<Widget />
</ErrorBoundary>
Error boundaries do not catch errors in event handlers, async code, or the server. Use try/catch there.
20. Styling
| Approach | Notes |
|---|---|
| Plain CSS |
import './App.css'. Global, class names can collide. |
| CSS Modules |
import s from './Btn.module.css', then className={s.primary}. Scoped automatically. |
| Tailwind CSS | Utility classes in JSX. Very popular, great speed. |
| Inline style |
style={{ color: 'red' }}. Good for dynamic values only. No pseudo-classes or media queries. |
| CSS-in-JS (styled-components, Emotion) | Still used, but runtime CSS-in-JS has trade-offs with Server Components. |
| Component libraries | shadcn/ui, MUI, Chakra, Mantine, Radix |
Conditional classes:
<button className={`btn ${active ? 'btn-active' : ''}`} />
// or with the clsx package
<button className={clsx('btn', { 'btn-active': active })} />
21. Routing
React itself has no router. The common choice is React Router (v7). Next.js has its own file-based routing.
import { createBrowserRouter, Link, NavLink, Outlet, useParams, useNavigate } from 'react-router';
import { RouterProvider } from 'react-router/dom';
const router = createBrowserRouter([
{
path: '/',
element: <Layout />, // renders <Outlet /> for children
children: [
{ index: true, element: <Home /> },
{ path: 'products/:id', element: <Product /> },
{ path: '*', element: <NotFound /> },
],
},
]);
createRoot(document.getElementById('root')).render(<RouterProvider router={router} />);
function Layout() {
return (
<>
<nav>
<NavLink to="/" className={({ isActive }) => (isActive ? 'active' : '')}>Home</NavLink>
</nav>
<Outlet />
</>
);
}
function Product() {
const { id } = useParams();
const navigate = useNavigate();
return <button onClick={() => navigate('/')}>Back from {id}</button>;
}
| Need | Use |
|---|---|
| Navigate without page reload |
<Link to="/x"> (never a plain <a> for internal links) |
| Active link styling | <NavLink> |
| Read URL params | useParams() |
Read or set ?q=...
|
useSearchParams() |
| Navigate from code | useNavigate() |
| Nested layout slot | <Outlet /> |
| 404 page | path: '*' |
The old reference sheets use v5/v6 APIs like
Switch,Redirect,useHistory. Those are gone. Today:Routes/route objects,<Navigate>,useNavigate. Olderreact-router-domimports still work in v6, and v7 re-exports them fromreact-router.
22. Data fetching and state management
What kind of state is it?
| Kind of state | Best tool |
|---|---|
| Local UI (open/closed, input) | useState |
| Complex local logic | useReducer |
| Shared by a few components | Lift state up, or Context |
| Server data (API responses) | TanStack Query (or SWR, or framework loaders) |
| Global client state (cart, UI prefs) | Zustand, Jotai, or Redux Toolkit |
| URL-driven state (filters, page) | Search params |
| Form state |
<form action>, or React Hook Form |
Most apps over-use global state. Server data is not global state. It is a cache.
TanStack Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function Todos({ userId }) {
const { data, isPending, error } = useQuery({
queryKey: ['todos', userId],
queryFn: async () => {
const res = await fetch(`/api/todos?user=${userId}`);
if (!res.ok) throw new Error('Failed to load');
return res.json();
},
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}
You get caching, deduping, retries, background refresh and invalidation for free.
Zustand (tiny global store)
import { create } from 'zustand';
const useCart = create(set => ({
items: [],
add: item => set(s => ({ items: [...s.items, item] })),
}));
const items = useCart(s => s.items); // subscribe to a slice only
Redux Toolkit (when you need the full toolbox)
Use Redux Toolkit, not plain Redux. createStore is deprecated and old tutorials still show it.
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; }, // Immer allows "mutation" here
addBy: (state, action) => { state.value += action.payload; },
},
});
export const { increment, addBy } = counterSlice.actions;
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });
import { Provider, useSelector, useDispatch } from 'react-redux';
<Provider store={store}><App /></Provider>
const value = useSelector(s => s.counter.value);
const dispatch = useDispatch();
dispatch(addBy(5));
For async data in Redux, use RTK Query or createAsyncThunk.
23. TypeScript with React
type ButtonProps = {
label: string;
variant?: 'primary' | 'danger';
onClick?: () => void;
children?: React.ReactNode;
};
function Button({ label, variant = 'primary', onClick }: ButtonProps) {
return <button className={variant} onClick={onClick}>{label}</button>;
}
| Need | Type |
|---|---|
| State that can be empty | `useState<User \ |
| DOM ref | {% raw %}useRef<HTMLInputElement>(null)
|
| Input change event | React.ChangeEvent<HTMLInputElement> |
| Form submit event | React.FormEvent<HTMLFormElement> |
| Click event | React.MouseEvent<HTMLButtonElement> |
| Any renderable child | React.ReactNode |
| Props of a native element | React.ComponentProps<'button'> |
| Style object | React.CSSProperties |
- You do not need
React.FC. Type the props directly. - Let TypeScript infer when it can. Annotate props, state that starts empty, and refs.
- Use discriminated unions for state:
{ status: 'loading' } | { status: 'success'; data: User }.
24. Testing
Stack: Vitest (or Jest) + React Testing Library + user-event + jest-dom.
npm i -D vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { test, expect } from 'vitest';
import Counter from './Counter';
test('increments the count on click', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /count: 0/i }));
expect(screen.getByRole('button', { name: /count: 1/i })).toBeInTheDocument();
});
Test like a user, not like a developer.
| Query priority | Example |
|---|---|
1. getByRole
|
getByRole('button', { name: /save/i }) |
2. getByLabelText
|
form fields |
3. getByText
|
visible text |
4. getByTestId
|
last resort |
| Prefix | Meaning |
|---|---|
getBy |
Must exist now, throws if missing |
queryBy |
Returns null if missing (assert absence) |
findBy |
Async, waits for it to appear |
Test pyramid: many unit tests, some integration tests, few end-to-end tests (Playwright or Cypress). Mock the network (MSW), not your own components.
25. Accessibility and security
Accessibility basics:
- Use semantic HTML:
<button>,<nav>,<main>,<label>. Not<div onClick>. - Every input needs a label:
<label htmlFor={id}>withuseId(). - Every informative image needs
alt. Decorative images getalt="". - Keep keyboard navigation working. Manage focus in modals (focus in, focus restored on close).
- Use ARIA only when native HTML cannot do the job.
- Add
eslint-plugin-jsx-a11y.
function Field({ label }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}
Security basics:
- Avoid
dangerouslySetInnerHTML. If you must, sanitize with DOMPurify. - Validate
hrefvalues from user input (blockjavascript:URLs). - Never put secrets in client code or
VITE_/NEXT_PUBLIC_variables. Anything in the bundle is public. - Keep tokens out of
localStoragewhen possible. PreferhttpOnlycookies. - Always re-check auth and validation on the server.
- Run
npm auditand keep dependencies and frameworks updated.
Gotchas that bite everyone
-
Stale state:
setCount(count + 1)repeated does not add up. UsesetCount(c => c + 1). -
Mutating state:
arr.push(x); setArr(arr)does nothing, same reference. -
{0 && <X/>}shows0. Compare explicitly. - Index as key on dynamic lists causes state jumping between items.
-
Missing dependencies in
useEffect. Trust the linter. - Missing cleanup (timers, listeners, subscriptions, fetch).
-
Race conditions in fetch effects. Use
AbortControlleror a library. -
Calling the handler:
onClick={handle()}runs on render. - Hooks inside conditions or loops. Breaks hook order.
- Defining a component inside another component. It remounts on every render and loses state.
-
Derived state in
useState:useState(props.value)ignores later prop changes. Derive instead. - Over-memoizing. Memoization has a cost. Profile first.
- Putting everything in Context or global state.
- Reading
ref.currentduring render. - Effect where an event handler belongs. If it responds to a click, it is not an effect.
Interview Rapid Fire
| Question | Short answer |
|---|---|
| What is React? | A JavaScript library for building UIs from components. Declarative, state-driven. |
| What is the Virtual DOM? | An in-memory description of the UI. React compares old and new (reconciliation) and updates only the real DOM parts that changed. |
| What is JSX? | Syntax sugar that compiles to jsx() calls producing element objects. |
| Props vs state? | Props come from the parent and are read-only. State is owned by the component and changes over time. |
| Controlled vs uncontrolled? | Controlled: React state drives the input value. Uncontrolled: the DOM holds the value, you read it via a ref or FormData. |
| Why are keys needed? | They identify items across renders so React reuses the right DOM and state. |
| Why can't you mutate state? | React compares by reference. Mutation skips updates and breaks features like memo. |
What does useEffect do? |
Syncs a component with an external system after render. Cleanup runs before re-run and on unmount. |
useMemo vs useCallback? |
useMemo caches a value. useCallback(fn, deps) is useMemo(() => fn, deps). |
useRef vs useState? |
Ref changes do not re-render. State changes do. |
| What triggers a re-render? | State change, parent re-render, context change. |
What is React.memo? |
Skips re-render when props are shallow-equal. |
| What is prop drilling and how do you avoid it? | Passing props through many layers. Use composition, Context, or a store. |
| What is lifting state up? | Moving state to the closest common parent so siblings can share it. |
| What are Higher-Order Components? | Functions that take a component and return a new one. Mostly replaced by hooks. |
| Rules of Hooks? | Top level only. Only in components or custom hooks. |
| What is reconciliation? | The diffing process that decides what changes in the DOM. |
| What is Strict Mode? | Dev-only checks: double-invokes renders and effects to find impurity and missing cleanup. |
| What is Suspense? | A boundary that shows a fallback while children are loading. |
| What are Server Components? | Components that render only on the server, send no JS to the client, and can read data directly. |
| Client vs Server Component? | Client: interactive, uses state and effects, needs 'use client'. Server: static or data-heavy, no interactivity. |
| What changed in React 19? | Actions, use, useActionState, useOptimistic, ref as prop, metadata tags, better errors. |
useEffect vs useLayoutEffect? |
The first runs after paint. The second runs before paint and blocks it. |
| How do you optimize a slow React app? | Profile, move state down, virtualize lists, code split, then memoize. Consider the React Compiler. |
| How do you handle errors? | Error boundaries for render errors. try/catch for handlers and async code. |
Why is fetch in useEffect risky? |
Race conditions, no caching, no dedupe, waterfalls. Prefer a data library or framework loaders. |
28. Legacy stuff you may still see
Class components. Still work, rarely written. Only needed for error boundaries.
| Class lifecycle | Hook equivalent (roughly) |
|---|---|
constructor / this.state
|
useState |
componentDidMount |
useEffect(fn, []) |
componentDidUpdate |
useEffect(fn, [deps]) |
componentWillUnmount |
cleanup function in useEffect
|
this.setState({...}) |
setState (class merges objects, hooks replace) |
static contextType |
useContext |
Other legacy patterns: HOCs, render props, React.createClass, ReactDOM.render, propTypes, Create React App, Redux createStore, React Router v5 Switch/Redirect/useHistory.
29. Official resources
- react.dev Learn: the best free path, with interactive examples
- react.dev API Reference: every hook and component
- You Might Not Need an Effect
- React Blog: release notes and security notices
- React Compiler docs
- MDN JavaScript and MDN Web Docs
- React Router docs, TanStack Query, Redux Toolkit, Testing Library
- Tools: React DevTools (browser extension),
eslint-plugin-react-hooks
Final tip
Do not try to memorize this in one sitting. Build a small project (todo app, then a dashboard with an API), come back here whenever you get stuck, and the patterns will stick.
If this helped, save it for your next revision or interview prep. Next up in the series: Next.js.
Found something outdated or wrong? Drop a comment and I will fix it.
Top comments (0)