What is React?
React is a JavaScript library for building user interfaces from components. A component is a function that returns markup, and you compose components together to build a page. Instead of telling the browser how to change the DOM step by step, you describe what the UI should look like for the current data, and React works out the updates.
That shift is called declarative rendering, and it is the reason React felt different in 2013. You stop writing “find this element, change its text, toggle that class” and start writing “given this state, here is the screen”. The messy part — diffing and updating the DOM efficiently — becomes React’s job.
Components and JSX
A React component is a function whose name starts with a capital letter and that returns JSX.
// Welcome.jsx
function Welcome({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default function App() {
return (
<main>
<Welcome name="Ada" />
<Welcome name="Grace" />
</main>
);
}
JSX looks like HTML but is JavaScript. Curly braces embed expressions, and the compiler translates each element into a function call. Because components are just functions, you can compose them, nest them and reuse them the same way you reuse any other function.
Props
Props are the inputs to a component. They flow down from parent to child and are read-only — a child must never mutate its props.
// Card.jsx
function Card({ title, children, variant = "default" }) {
return (
<article className={`card card--${variant}`}>
<h3>{title}</h3>
{children}
</article>
);
}
The children prop is whatever you nest between the opening and closing tags, which makes components composable wrappers. Default values let callers omit props that usually stay the same.
State with useState
Props are fixed for a given render. State is data a component owns and can change over time. The useState hook creates a state value and a setter.
// Search.jsx
import { useState } from "react";
function Search({ onSearch }) {
const [query, setQuery] = useState("");
return (
<form onSubmit={(e) => { e.preventDefault(); onSearch(query); }}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</form>
);
}
Calling the setter schedules a re-render with the new value. State updates are asynchronous and batched, and they replace rather than merge, so always create a new object or array when updating structured state.
// update.jsx
setUser((prev) => ({ ...prev, name: "Grace" }));
setItems((prev) => [...prev, newItem]);
Using the updater function form keeps you safe when the next value depends on the previous one.
Rendering lists and conditions
React has no template language for loops and branches — you use plain JavaScript.
// List.jsx
function List({ items }) {
if (items.length === 0) return <p>Nothing here yet.</p>;
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.label}</li>
))}
</ul>
);
}
The key prop is required for list items. Give it a stable, unique value from your data so React can match items correctly across renders. The ternary operator and && cover inline conditions, though early returns often read better.
Events
React events use camelCase props and receive a synthetic event object.
// Button.jsx
<button
onClick={(event) => {
event.preventDefault();
submit();
}}
>
Save
</button>
Passing a function to the prop is important — onClick={submit()} would call it immediately during render. When a handler needs an argument, wrap it in an arrow function.
Effects with useEffect
useEffect synchronises your component with systems outside React: network requests, subscriptions, timers and direct DOM work.
// Users.jsx
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch("/api/users", { signal: controller.signal })
.then((res) => res.json())
.then(setUsers)
.catch((err) => {
if (err.name !== "AbortError") console.error(err);
});
return () => controller.abort();
}, []);
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
The dependency array controls when the effect re-runs: an empty array runs once after mount, a list of values runs when any of them change, and no array runs after every render. The returned function is cleanup, which runs before the next effect and on unmount. Always clean up subscriptions and in-flight requests.
Context and refs
Context shares a value with a whole subtree without prop drilling.
// theme.jsx
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
Refs hold a mutable value or a DOM node that should not trigger a re-render.
// focus.jsx
const inputRef = useRef(null);
useEffect(() => inputRef.current?.focus(), []);
return <input ref={inputRef} />;
Data flow and lifting state
React data flows in one direction. When two siblings need to share state, you lift it to their closest common parent and pass the value down as a prop and the setter down as a callback. This keeps a single source of truth and makes the flow of change easy to follow.
For deeper trees, context or a dedicated state library such as Zustand or Redux may be worth the extra machinery. Reach for them when prop passing becomes painful, not before.
Hooks and custom hooks
Hooks are functions that let components tap into React features. They must be called at the top level, never inside conditions or loops. Once you understand them, you can extract shared logic into a custom hook.
// useFetch.js
function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let active = true;
fetch(url)
.then((res) => res.json())
.then((json) => active && setData(json))
.catch((err) => active && setError(err));
return () => { active = false; };
}, [url]);
return { data, error };
}
A custom hook is just a function whose name starts with use and that calls other hooks. It shares logic, not state — each component that uses it gets its own independent copy.
Best practices
- Keep components small and focused on one responsibility.
- Compute derived values during render instead of storing them in state.
- Use the updater form of a setter when the next value depends on the previous one.
- Give every list item a stable key from your data.
- Clean up effects, especially subscriptions and pending requests.
- Lift state only as far as it needs to go.
- Extract repeated logic into custom hooks.
- Reach for memoisation (
useMemo,useCallback,memo) only after measuring a real problem.
Common mistakes
- Mutating state directly instead of creating a new value.
- Using the array index as a key and breaking reordering.
- Putting derived data in state and syncing it with an effect.
- Forgetting the dependency array and causing infinite loops.
- Calling a hook conditionally and breaking the rules of hooks.
- Optimising with memoisation before there is a performance issue.
Where to go next
React is the view layer; Next.js adds routing, data fetching and server rendering on top. Compare the model with Vue and Svelte, and strengthen your foundations with JavaScript and TypeScript. The interactive React tutorial walks through the same ideas step by step.