cheat sheet
React Basics
Foundational React patterns — function components, JSX, props, hooks (useState, useEffect, useRef, useContext), list rendering, and form handling — with TypeScript throughout.
React Basics — Function components, hooks, and JSX
What it is
React is a JavaScript library for building user interfaces by composing reusable components. It is developed by Meta and a large open-source community, written in TypeScript, and runs anywhere JavaScript runs — browser, server (via frameworks like Next.js or Remix), and native (via React Native). Modern React is "function components plus hooks": no classes, no lifecycle methods, just plain functions that return JSX and call hooks to read state and side-effects. Alternatives in the same niche include Vue, Svelte, and Solid — but React's ecosystem and hiring market remain the broadest.
Install
There is no react CLI — you scaffold a React project with a build tool. Vite is the most common modern starter; Next.js is the dominant meta-framework. For an existing project, install react and react-dom together.
# Brand new Vite + React + TypeScript project
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
# Add React to an existing project
npm install react react-dom
npm install -D @types/react @types/react-dom
Output: (none — exits 0 on success)
Hello, function component
A function component is a JavaScript function that returns a piece of UI as JSX. It receives data through props (its single argument) and renders whatever JSX the function evaluates to. The component name must start with a capital letter — that is how the JSX compiler tells <Hello /> (a component) apart from <div> (a DOM tag).
// src/components/Hello.tsx
type HelloProps = { name: string };
export function Hello({ name }: HelloProps) {
return <h1>Hello, {name}!</h1>;
}
// src/main.tsx — mount it
import { createRoot } from "react-dom/client";
import { Hello } from "./components/Hello";
const root = createRoot(document.getElementById("root")!);
root.render(<Hello name="Alice Dev" />);
Output: (rendered to the DOM)
<h1>Hello, Alice Dev!</h1>
JSX
JSX is an HTML-like syntax that compiles to React.createElement(...) calls. Anything inside {} is a JavaScript expression that is evaluated and inserted. Attributes follow JS naming (className, not class; htmlFor, not for; onClick, not onclick). A component must return a single root node — wrap multiple siblings in a <>...</> fragment if you do not want an extra DOM element.
function Greeting({ user }: { user: { name: string; admin: boolean } }) {
const now = new Date().toLocaleTimeString();
return (
<>
<h2 className="title">Hello, {user.name}</h2>
<p>Time: {now}</p>
{user.admin && <span className="badge">admin</span>}
<ul style={{ listStyle: "square" }}>
{[1, 2, 3].map((n) => (
<li key={n}>Item {n}</li>
))}
</ul>
</>
);
}
Output: (rendered HTML)
<h2 class="title">Hello, Alice Dev</h2>
<p>Time: 09:42:13</p>
<span class="badge">admin</span>
<ul style="list-style: square;">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
Props and children
Props are the read-only inputs to a component, passed as JSX attributes. The special children prop receives whatever JSX is nested between the component's opening and closing tags — this is how layout/wrapper components work. Type props with a TS interface or type alias for safety and editor autocomplete.
import type { ReactNode } from "react";
type CardProps = {
title: string;
variant?: "default" | "warning" | "danger";
children: ReactNode;
};
export function Card({ title, variant = "default", children }: CardProps) {
return (
<section className={`card card-${variant}`}>
<header>{title}</header>
<div className="card-body">{children}</div>
</section>
);
}
// Usage
function App() {
return (
<Card title="Account" variant="warning">
<p>Your trial ends in 3 days.</p>
<button>Upgrade</button>
</Card>
);
}
Output: (rendered HTML)
<section class="card card-warning">
<header>Account</header>
<div class="card-body">
<p>Your trial ends in 3 days.</p>
<button>Upgrade</button>
</div>
</section>
Common prop types
| Type | Purpose |
|---|---|
string, number, boolean | Primitive props |
ReactNode | Anything renderable (string, number, element, array, null) |
ReactElement | A single JSX element |
(e: React.MouseEvent) => void | Click handlers |
(e: React.ChangeEvent<HTMLInputElement>) => void | Input change handlers |
React.CSSProperties | Inline style objects |
React.ComponentProps<"button"> | All native props of a tag |
useState
useState adds local mutable state to a function component. It returns a [value, setter] tuple: read value to render, call setter(next) to schedule a re-render with the new value. The setter is batched and asynchronous — calling it does not change the current value variable inside the same function call. To compute the next value from the previous, pass a function: setCount(c => c + 1).
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount((c) => c + 1)}>+1 (functional)</button>
<button onClick={() => setCount(0)}>reset</button>
</div>
);
}
Output: (after clicking +1 three times)
Count: 3
Lazy initial state
If the initial value is expensive to compute, pass a function instead of a value. React will only call it on the first render.
const [users, setUsers] = useState<User[]>(() => {
const cached = localStorage.getItem("users");
return cached ? JSON.parse(cached) : [];
});
Output: (none — exits 0 on success)
State with objects and arrays
State must be treated as immutable — never mutate it, always create a new object/array. React compares state with Object.is, so a mutation goes unnoticed and no re-render happens.
const [user, setUser] = useState({ name: "Alice Dev", age: 30 });
// WRONG — mutation, no re-render
// user.age = 31;
// RIGHT — new object
setUser({ ...user, age: 31 });
setUser((prev) => ({ ...prev, age: prev.age + 1 }));
const [todos, setTodos] = useState<string[]>([]);
setTodos((prev) => [...prev, "buy milk"]); // append
setTodos((prev) => prev.filter((t) => t !== "buy milk")); // remove
Output: (none — exits 0 on success)
useEffect
useEffect runs side effects (network requests, subscriptions, DOM mutations, timers) after the component renders. The second argument is a dependency array: the effect re-runs only when one of those values changes. Return a cleanup function from the effect to undo subscriptions/timers when the dependencies change or the component unmounts.
import { useState, useEffect } from "react";
export function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(id); // cleanup
}, []); // empty deps → run once on mount
return <p>Current time: {time.toLocaleTimeString()}</p>;
}
Output: (rendered, updates every second)
Current time: 09:42:13
Current time: 09:42:14
Current time: 09:42:15
Dependency array rules
| Pattern | When effect runs |
|---|---|
useEffect(fn) | After every render (rare — usually a bug) |
useEffect(fn, []) | Once after mount; cleanup on unmount |
useEffect(fn, [a, b]) | After mount + whenever a or b changes |
Fetching data
import { useState, useEffect } from "react";
type User = { id: number; name: string };
export function UserCard({ id }: { id: number }) {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then((r) => r.json())
.then(setUser)
.catch((e) => {
if (e.name !== "AbortError") setError(String(e));
});
return () => ctrl.abort(); // cancel on unmount / id change
}, [id]);
if (error) return <p>Error: {error}</p>;
if (!user) return <p>Loading...</p>;
return <p>{user.name}</p>;
}
Output: (rendered)
Loading...
Alice Dev
useRef
useRef returns a mutable container { current: T } that persists across renders without triggering a re-render when you change .current. Two main uses: holding a DOM node reference (pass to a JSX ref attribute) and storing a mutable value that should not cause re-renders (timer IDs, previous values, scroll positions).
import { useRef, useEffect } from "react";
export function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="auto-focused" />;
}
export function StopwatchToggle() {
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const start = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => console.log("tick"), 1000);
};
const stop = () => {
if (!intervalRef.current) return;
clearInterval(intervalRef.current);
intervalRef.current = null;
};
return (
<>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
Output: (console after starting and waiting 3 seconds)
tick
tick
tick
useContext + createContext
Context lets you share a value with every component beneath a provider without passing props at every level. Create a context with createContext(default), wrap a tree with <MyContext.Provider value={...}>, and read it with useContext(MyContext) anywhere inside. Context is best for low-frequency, app-wide values (theme, current user, locale) — not for state that changes on every keystroke (use Zustand/Redux/Jotai for that, or co-locate state higher up).
import { createContext, useContext, useState, type ReactNode } from "react";
type Theme = "light" | "dark";
type ThemeContextValue = { theme: Theme; toggle: () => void };
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
return ctx;
}
// Usage anywhere in the tree
function ThemeToggleButton() {
const { theme, toggle } = useTheme();
return <button onClick={toggle}>Theme: {theme}</button>;
}
Output: (after one click)
Theme: dark
List rendering and key
When you render an array of items with .map(), React needs a stable, unique key prop on each element so it can match up items across renders and reorder/insert/delete efficiently. Use the item's natural ID — never the array index unless the list is truly static (no inserts, deletes, or reorders), because index keys cause state to leak between items when the list changes.
type Todo = { id: string; text: string; done: boolean };
export function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.text}
</li>
))}
</ul>
);
}
Output: (rendered)
<ul>
<li style="text-decoration: none;">buy milk</li>
<li style="text-decoration: line-through;">walk the dog</li>
</ul>
Conditional rendering
JSX is just JavaScript, so any expression works for branching: ternaries, &&, early returns, or helper functions.
function Status({ user }: { user: { loggedIn: boolean; admin: boolean } | null }) {
// Early return
if (!user) return <p>Not signed in.</p>;
return (
<>
{/* Ternary */}
{user.loggedIn ? <p>Welcome!</p> : <p>Please sign in.</p>}
{/* && — render-or-nothing */}
{user.admin && <button>Admin panel</button>}
{/* Nullish coalescing for empty state */}
<p>{user.admin ? "admin" : "user"}</p>
</>
);
}
Output: (rendered for an admin user)
<p>Welcome!</p>
<button>Admin panel</button>
<p>admin</p>
Pitfall: 0 && ...
{count && <X />} renders the number 0 when count is 0 — JSX renders numbers as text. Use count > 0 && <X /> or !!count && <X /> instead.
{count > 0 && <p>{count} items</p>}
Output: (none — exits 0 on success)
Event handlers and synthetic events
React wraps native DOM events in a cross-browser SyntheticEvent object. Handlers are passed by JSX attribute (onClick, onChange, onSubmit, …). The synthetic event has the same shape as the native one (.target, .preventDefault(), .stopPropagation()) plus React-specific helpers.
import type { MouseEvent, FormEvent } from "react";
function Button() {
function handleClick(e: MouseEvent<HTMLButtonElement>) {
e.preventDefault();
console.log("clicked", e.currentTarget.dataset.id);
}
return <button data-id="42" onClick={handleClick}>Click me</button>;
}
function LoginForm() {
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const data = new FormData(e.currentTarget);
console.log({
email: data.get("email"),
password: data.get("password"),
});
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Sign in</button>
</form>
);
}
Output: (console after submission)
{ email: 'alice@example.com', password: 'hunter2' }
Common event types
| Event | TS type |
|---|---|
| Click | React.MouseEvent<HTMLButtonElement> |
| Input change | React.ChangeEvent<HTMLInputElement> |
| Form submit | React.FormEvent<HTMLFormElement> |
| Keyboard | React.KeyboardEvent<HTMLInputElement> |
| Focus | React.FocusEvent<HTMLInputElement> |
Controlled vs uncontrolled inputs
A controlled input binds its value to state and updates that state in onChange: React is the single source of truth. An uncontrolled input lets the DOM keep the value internally and you read it with a ref or FormData on submit. Prefer controlled inputs when you need live validation, conditional rendering based on the value, or multiple synchronized fields; prefer uncontrolled for plain forms where you only care about the final value at submit time.
import { useState, useRef } from "react";
// Controlled — value bound to state
export function ControlledEmail() {
const [email, setEmail] = useState("");
const valid = email.includes("@");
return (
<>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{!valid && email && <p>Invalid email</p>}
<button disabled={!valid}>Submit</button>
</>
);
}
// Uncontrolled — read via ref
export function UncontrolledEmail() {
const ref = useRef<HTMLInputElement>(null);
return (
<>
<input ref={ref} type="email" defaultValue="" />
<button onClick={() => console.log(ref.current?.value)}>
Log value
</button>
</>
);
}
Output: (console after typing then clicking the uncontrolled button)
alice@example.com
A typed form with Zod validation
Combine controlled inputs with Zod for end-to-end type safety: define the schema, derive the type, validate on submit, and render errors.
import { useState } from "react";
import { z } from "zod";
const SignupSchema = z.object({
email: z.string().email("must be a valid email"),
password: z.string().min(8, "at least 8 characters"),
age: z.coerce.number().int().min(13, "must be 13 or older"),
});
type Signup = z.infer<typeof SignupSchema>;
type Errors = Partial<Record<keyof Signup, string>>;
export function SignupForm() {
const [form, setForm] = useState({ email: "", password: "", age: "" });
const [errors, setErrors] = useState<Errors>({});
function update<K extends keyof typeof form>(key: K, value: string) {
setForm((prev) => ({ ...prev, [key]: value }));
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const result = SignupSchema.safeParse(form);
if (!result.success) {
const fieldErrors: Errors = {};
for (const issue of result.error.issues) {
fieldErrors[issue.path[0] as keyof Signup] = issue.message;
}
setErrors(fieldErrors);
return;
}
setErrors({});
console.log("submit", result.data);
}
return (
<form onSubmit={handleSubmit}>
<label>
Email
<input
value={form.email}
onChange={(e) => update("email", e.target.value)}
/>
{errors.email && <small className="err">{errors.email}</small>}
</label>
<label>
Password
<input
type="password"
value={form.password}
onChange={(e) => update("password", e.target.value)}
/>
{errors.password && <small className="err">{errors.password}</small>}
</label>
<label>
Age
<input
value={form.age}
onChange={(e) => update("age", e.target.value)}
/>
{errors.age && <small className="err">{errors.age}</small>}
</label>
<button type="submit">Sign up</button>
</form>
);
}
Output: (console on a valid submission)
submit { email: 'alice@example.com', password: 'hunter2!!', age: 30 }
The React Compiler (overview)
The React Compiler (React 19+) is an opt-in build-time tool that automatically memoizes components and hooks based on dataflow analysis — eliminating most hand-written useMemo / useCallback / memo boilerplate. Add the babel-plugin-react-compiler (or its SWC equivalent in your bundler config); your existing source code is unchanged, and the compiler inserts memoization where it is provably safe. As long as your components follow the Rules of React (no mutation of props/state, no side effects in render), you get free performance with no code changes.
# Babel toolchain
npm install -D babel-plugin-react-compiler
# Or via SWC (Vite)
npm install -D @swc/plugin-react-compiler
Output: (none — exits 0 on success)
// vite.config.ts — enable in Vite + SWC
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
export default defineConfig({
plugins: [
react({
plugins: [["@swc/plugin-react-compiler", {}]],
}),
],
});
Common pitfalls
- Mutating state —
users.push(x)instead ofsetUsers([...users, x]). React compares withObject.is; a mutated reference looks unchanged, so no re-render. - Stale closures in effects — referencing state inside a
setInterval/setTimeoutwithout putting it in the deps array. Use a functional updater (setX(prev => …)) or add the value to deps. - Missing
keyor using array index — index keys break when the list reorders. Always use a stable ID. - Component name starts lowercase —
<myComponent />is treated as a DOM tag and silently rendered as<mycomponent>. Always capitalize. useStateinitial value runs every render —useState(expensiveCompute())callsexpensiveCompute()on every render (the result is discarded). UseuseState(() => expensiveCompute()).- Effect running on every render — forgetting the deps array.
useEffect(fn)(no second arg) runs after every render, causing infinite fetches. - Setting state during render —
setX()in the function body without a guard creates an infinite re-render loop. Only call setters from event handlers, effects, or initialization. {value && <X />}whenvalueis0— renders the literal0. Usevalue > 0 && <X />or!!value && <X />.- Forgetting cleanup in effects — subscribers, timers, and AbortControllers must be cleaned up in the returned function or you leak memory / get duplicate handlers in dev (Strict Mode mounts twice).
- Conditional hooks — never call a hook inside
if/for/early return. React relies on call order; conditional hooks throw "Rendered fewer/more hooks than during the previous render".
Real-world recipes
Debounced search input
A controlled text input that fires an API request 300 ms after the user stops typing — the canonical debounce pattern, expressed as an effect.
import { useState, useEffect } from "react";
export function SearchBox() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<string[]>([]);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const ctrl = new AbortController();
const id = setTimeout(async () => {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: ctrl.signal,
});
const json: string[] = await res.json();
setResults(json);
}, 300);
return () => {
clearTimeout(id);
ctrl.abort();
};
}, [query]);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="search…" />
<ul>{results.map((r) => <li key={r}>{r}</li>)}</ul>
</>
);
}
Output: (console while typing "ali")
GET /api/search?q=ali -> ["alice", "alicia", "alistair"]
Persistent state with localStorage
A custom hook that mirrors a piece of state to localStorage so it survives reloads — composing useState and useEffect into a reusable building block.
import { useState, useEffect } from "react";
function usePersistentState<T>(key: string, initial: T): [T, (v: T) => void] {
const [value, setValue] = useState<T>(() => {
const cached = localStorage.getItem(key);
return cached ? (JSON.parse(cached) as T) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function App() {
const [theme, setTheme] = usePersistentState<"light" | "dark">("theme", "light");
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Theme: {theme}
</button>
);
}
Output: (after toggling and reloading the page)
Theme: dark
Modal portal with focus management
A reusable modal: rendered into document.body via createPortal, autofocuses its first input on mount, and traps Escape to close. Combines useRef, useEffect, and props.
import { useEffect, useRef, type ReactNode } from "react";
import { createPortal } from "react-dom";
type ModalProps = { open: boolean; onClose: () => void; children: ReactNode };
export function Modal({ open, onClose, children }: ModalProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
ref.current?.querySelector<HTMLElement>("input,button,select,textarea")?.focus();
const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, onClose]);
if (!open) return null;
return createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div ref={ref} className="modal" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body,
);
}
Output: (rendered into document.body when open)
<div class="modal-backdrop">
<div class="modal">…</div>
</div>
Pairing with Vite or Bun
The fastest way to start: Vite (most popular) or Bun (single-binary, slightly faster install).
# Vite + React + TypeScript + SWC compiler
npm create vite@latest my-app -- --template react-swc-ts
# Bun + React (uses Bun's native bundler/server)
bun create react my-app
Output:
✔ Project name: my-app
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC
Scaffolding project in /home/alice/my-app...
Done. Now run:
cd my-app
npm install
npm run dev