Components, props, state, and the hooks that power modern React apps.
JSX lets you write markup that looks like HTML directly inside JavaScript: const el = <h1>Hello!</h1>. Under the hood, a build tool (Babel, or Next.js's compiler) transforms it into plain JavaScript function calls — JSX is a convenience, not something browsers understand natively.
Curly braces {} embed a real JavaScript expression inside JSX — {name}, {1 + 1}, {items.length} all work, since each is a single expression that evaluates to a value. A full statement (if, for, a variable declaration) can't go directly inside {} — JSX only accepts expressions.
Every JSX element must have exactly one root/outermost element — two sibling elements at the top level is a compile error. When there's no single natural wrapper, a Fragment (<>...</>) groups multiple elements without adding an extra DOM node.
const name = "Ada";
const el = <h1>Hello, {name}!</h1>;return (
<>
<h1>Title</h1>
<p>Body text</p>
</>
);A component is just a function that returns JSX, named starting with a capital letter (Welcome, not welcome) — that capitalization is exactly how React and JSX tell your custom components apart from built-in HTML tags like <div>.
Props are how a parent passes data into a child: <Welcome name="Ada" /> passes { name: "Ada" } as a single object, which the component receives as its one function argument — destructuring it in the parameter list ({ name }) is the common, readable way to read specific props.
Props flow one direction only, parent to child — a component can read the props it was given, but can never reassign or modify them. If a value needs to change over time, that's what state (next section) is for, not props.
function Badge({ label, color }) {
return <span style={{ color }}>{label}</span>;
}<Badge label="New" color="teal" />useState gives a component a piece of memory that survives across re-renders — const [count, setCount] = useState(0) returns a pair: the current value, and a function to update it. Both names are yours to choose; only the pairing and order (value, then setter) is fixed.
Calling the setter doesn't just update a plain variable — it tells React to re-render the component with the new value. A component's regular local variables reset to their initial expression on every render; state is specifically what persists between them.
The value passed to useState(...) is only used once — the very first time the component renders. After that, the state's current value is whatever it was most recently set to, completely independent of that initial argument.
const [isOpen, setIsOpen] = useState(false);
setIsOpen(!isOpen);setCount((prev) => prev + 1);React event handler props are camelCase, not lowercase: onClick, onChange, onSubmit — the plain lowercase HTML versions (onclick) simply do nothing in JSX, since React defines its own consistent naming for every event.
A handler prop always takes a function, never the result of calling one — onClick={() => setCount(count + 1)} passes a function to run *later*, on click. Writing onClick={setCount(count + 1)} instead calls it immediately during render, a very common early mistake.
The event object React passes to a handler (onChange={(e) => ...}) is a SyntheticEvent — a cross-browser wrapper around the native browser event, normalized so the same code behaves consistently regardless of which browser is running it.
<button onClick={() => setCount(count + 1)}>+1</button><input onChange={(e) => setText(e.target.value)} />JSX can only contain expressions, not statements — an if/else block doesn't evaluate to a value, so it can't be dropped directly inside {}. A ternary (condition ? a : b) does evaluate to a value, making it the standard way to choose between two outputs inline.
For an all-or-nothing case — show something, or show nothing — {condition && <Component />} is the more common shorthand: && short-circuits to render nothing at all when condition is false, with no separate else branch needed.
A well-known && gotcha: {count && <p>{count} items</p>} renders a literal 0 on screen when count is exactly 0, since 0 is falsy but is still what && returns. Writing {count > 0 && ...} (an actual boolean) avoids it.
{isOnline ? "Online" : "Offline"}{unreadCount > 0 && <Badge count={unreadCount} />}Array.map() is the standard way to turn a list of data into a list of elements: {items.map((item) => <li key={item.id}>{item.text}</li>)} — the same .map() from plain JavaScript, just returning JSX instead of plain values.
key is a special prop, read only by React itself (never passed down to your actual component), that lets React track which rendered item is which across re-renders — without a stable key, React falls back to comparing by position, which can misattribute state after an insertion, deletion, or reorder.
key must be unique only among *sibling* elements in that specific list, not globally unique across the whole app — two unrelated lists elsewhere on the page can safely reuse the same key values with no conflict.
const todos = [{ id: 1, text: "Milk" }, { id: 2, text: "Eggs" }];
todos.map((t) => <li key={t.id}>{t.text}</li>);useEffect runs code *after* a render commits to the screen — for anything that reaches outside React's own rendering, like fetching data, subscribing to an event, or manually working with a DOM API. useEffect(() => { ... }, [deps]) takes a function to run and a dependency array controlling when it re-runs.
The dependency array is the part that trips people up most: [] (empty) means "run once, after the first render only"; [count] means "re-run whenever count changes"; omitting the array entirely means "run after every single render," almost never what you actually want.
Returning a function from inside the effect defines cleanup — React calls it right before the effect re-runs, and once more when the component is removed entirely. This is how you unsubscribe from something you subscribed to, preventing a real memory leak.
useEffect(() => {
console.log("mounted");
}, []);useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);A "controlled" input has its value driven entirely by React state, not by the DOM itself: <input value={text} onChange={(e) => setText(e.target.value)} /> — every keystroke updates state, and state is what determines what the input displays, keeping React as the single source of truth.
Without the onChange handler, a controlled input (one with a value prop) becomes read-only — React will keep resetting the displayed value back to whatever's in state, ignoring what the user actually typed, since there's nothing telling state to update.
checkboxes use checked instead of value (checked={isAgreed}, with an onChange to flip it), and a <select> uses value on the <select> element itself rather than a selected attribute on the individual <option>s — small but real differences from plain HTML forms.
const [text, setText] = useState("");
<input value={text} onChange={(e) => setText(e.target.value)} /><input type="checkbox" checked={isAgreed} onChange={(e) => setIsAgreed(e.target.checked)} />Real React apps are built by composing many small components together, not one giant component — a Page might render a Header, a List of Card components, and a Footer, each handling its own small piece of the UI.
When two sibling components both need access to the same changing value, the fix is "lifting state up": move the useState call to their closest common parent, then pass the value and its setter down as props to both children — rather than trying to have one child talk directly to another.
The children prop (mentioned in Components & Props) is what makes generic wrapper components possible — a Card component can render <div className="card">{children}</div> and work with any content placed between its tags, without knowing in advance what that content will be.
function Parent() {
const [value, setValue] = useState("");
return (
<>
<Input value={value} onChange={setValue} />
<Preview value={value} />
</>
);
}A custom hook is just a regular JavaScript function whose name starts with use, that itself calls other hooks (useState, useEffect, etc.) inside it — a way to extract and reuse stateful logic between components, without repeating the same useState/useEffect pair everywhere it's needed.
The use prefix isn't just convention for readability — React's own tooling (and the linter rule that ships with React) uses it to know which functions need to follow the Rules of Hooks (like never calling a hook conditionally), so skipping the prefix on a function that calls hooks internally will cause real, hard-to-diagnose lint gaps.
A custom hook returns whatever the component calling it needs — often an array like useState's own [value, setter] pattern, or an object with several named values — and every component that calls it gets its own, completely independent copy of that state.
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue((v) => !v);
return [value, toggle];
}