Getting Up to Date with React
React has shipped a paradigm-shifting feature almost every year since its open-source debut in 2013. If you stepped away for a while, the codebase you return to will look nothing like the one you left. This post walks through every era, explains why each change mattered, and — where possible — lets you interact with a live demo right here in the page.
2013–2015 · The Birth — JSX, Virtual DOM, Components
React introduced three ideas that were radical at the time:
- JSX — write HTML-like syntax directly in JavaScript.
- Virtual DOM — diff a lightweight tree instead of touching the real DOM.
- One-way data flow — props flow down, events flow up.
// The original "Hello World" — still valid today
const element = <h1>Hello, {name}!</h1>;
ReactDOM.render(element, document.getElementById('root'));
The component model made UI a function of state: given the same props, a component always renders the same output. This was the foundation everything else was built on.
2016–2017 · The Class Component Era
Before hooks, class components were the only way to hold state or respond to lifecycle events.
class Timer extends React.Component {
state = { seconds: 0 };
componentDidMount() {
this.interval = setInterval(() => {
this.setState(s => ({ seconds: s.seconds + 1 }));
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <p>Elapsed: {this.state.seconds}s</p>;
}
}
This era also popularised two powerful composition patterns:
- Higher-Order Components (HOCs) — functions that wrap a component to inject behaviour (
withRouter,connect). - Render Props — components that expose their internal logic through a function child.
// HOC pattern (e.g. react-router v4)
export default withRouter(function Nav({ history }) {
return <button onClick={() => history.push('/home')}>Go Home</button>;
});
The main criticism? "Wrapper hell" — deeply nested HOCs made the component tree unreadable in DevTools, and sharing stateful logic between components was verbose.
2018 · Context API (Modern) & Portals
React 16.3 shipped the new Context API, replacing the old, unstable one. It provided a first-class way to share values across the tree without prop drilling.
const ThemeCtx = React.createContext('light');
function App() {
return (
<ThemeCtx.Provider value="dark">
<Toolbar />
</ThemeCtx.Provider>
);
}
// Deep child — no prop drilling needed
function ThemedButton() {
return (
<ThemeCtx.Consumer>{theme => <button className={theme}>Click me</button>}</ThemeCtx.Consumer>
);
}
Portals (ReactDOM.createPortal) also landed here, allowing components to render children into a different DOM node — essential for modals, tooltips, and toast notifications.
2019 · Hooks — The Paradigm Shift
React 16.8 introduced Hooks, the single biggest API change in React's history. They let functional components hold state, run side effects, and compose logic — all without classes.
| Hook | Purpose |
|---|---|
useState |
Local state |
useEffect |
Side effects (fetch, subscriptions, timers) |
useContext |
Read context without a Consumer wrapper |
useRef |
Mutable ref that persists across renders |
useMemo / useCallback |
Memoisation |
useReducer |
Complex state machines |
Why it mattered
- Custom hooks let you extract and reuse stateful logic as plain functions (
useFetch,useLocalStorage,useDebounce). - No more
thisbinding issues. - The codebase moves from OOP to functional composition.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
return width;
}
Live Demo — useState + useEffect
2020–2021 · Concurrent Features & Suspense
React 17 was a "stepping-stone" release with no new developer-facing features (it focused on the new JSX transform and gradual upgrades). But behind the scenes, the Concurrent Mode architecture was being built.
The first user-visible feature was React.lazy + <Suspense>:
const LazyDashboard = React.lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<LazyDashboard />
</Suspense>
);
}
This let you code-split at the component level with zero configuration — Webpack (or any bundler) automatically creates a new chunk for the dynamic import.
Live Demo — React.lazy + Suspense
2022 · React 18 — Concurrent Rendering for Real
React 18 made concurrent rendering opt-in and production-ready. Key additions:
createRoot (New Root API)
// Before (legacy)
ReactDOM.render(<App />, document.getElementById('root'));
// After (React 18+)
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
useTransition — Mark Non-Urgent Updates
function SearchResults({ query }) {
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
function handleChange(e) {
// Typing is urgent — update the input immediately
setInputValue(e.target.value);
// Filtering thousands of items is NOT urgent
startTransition(() => {
setResults(filterItems(e.target.value));
});
}
return (
<>
<input onChange={handleChange} />
{isPending ? <Spinner /> : <List items={results} />}
</>
);
}
Automatic Batching
In React 17, state updates inside setTimeout or fetch callbacks were not batched. In React 18, all updates are batched by default, reducing unnecessary re-renders.
// React 18: these two setState calls produce ONE re-render
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 18 batches → 1 render
// React 17 → 2 renders
}, 1000);
2023 · React Server Components (RSC)
Server Components are arguably the most architectural shift since hooks. With the React + Next.js 13+ App Router, components are server-rendered by default — they never ship JavaScript to the client.
The Mental Model
Server Component → Can read DB, access filesystem, fetch APIs at build/request time
Ships 0 KB of JS to the browser
Cannot use hooks or browser APIs
Client Component → "use client" directive at the top of the file
Runs in the browser, can use useState, onClick, etc.
Server Actions
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title');
await db.posts.create({ title });
revalidatePath('/posts');
}
// app/page.tsx (Server Component)
import { createPost } from './actions';
export default function Page() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}
No API route, no fetch, no loading state boilerplate. The form calls a server function directly.
2024+ · React 19 — The Latest Era
React 19 ships new primitives that dramatically reduce boilerplate:
use() — Read Promises & Context in Render
// Before: useEffect + useState + loading state
// After:
function Comments({ commentsPromise }) {
const comments = use(commentsPromise);
return comments.map(c => <p key={c.id}>{c.body}</p>);
}
Unlike await, use() can be called conditionally and works with Suspense boundaries.
useActionState — Forms Without Boilerplate
async function updateName(prevState, formData) {
const name = formData.get('name');
const error = await saveName(name);
if (error) return { error };
return { error: null };
}
function NameForm() {
const [state, formAction, isPending] = useActionState(updateName, null);
return (
<form action={formAction}>
<input name="name" />
<button disabled={isPending}>Save</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
useOptimistic — Instant UI Feedback
function MessageList({ messages, sendMessage }) {
const [optimistic, addOptimistic] = useOptimistic(messages, (state, newMsg) => [
...state,
{ text: newMsg, pending: true },
]);
async function handleSubmit(formData) {
const text = formData.get('message');
addOptimistic(text); // Instant UI update
await sendMessage(text); // Actual server call
}
return (
<>
{optimistic.map((m, i) => (
<p key={i} style={{ opacity: m.pending ? 0.5 : 1 }}>
{m.text}
</p>
))}
<form action={handleSubmit}>
<input name="message" />
</form>
</>
);
}
React Compiler (React Forget)
The React Compiler automatically memoises components and hooks. You no longer need to manually wrap things in useMemo, useCallback, or React.memo — the compiler figures out the minimal set of values that need to be tracked.
// Before React Compiler — manual memoisation everywhere:
const filtered = useMemo(() => items.filter(filterFn), [items, filterFn]);
const handleClick = useCallback(() => {
/* ... */
}, [dep]);
// After React Compiler — just write plain code:
const filtered = items.filter(filterFn); // compiler handles it
const handleClick = () => {
/* ... */
};
Live Demo — use() + useActionState
The Trajectory
Looking at the timeline, a clear pattern emerges:
| Era | Theme |
|---|---|
| 2013–2017 | Declarative UI — Components, Virtual DOM, one-way data flow |
| 2018–2019 | Composition — Context, Hooks, custom hooks |
| 2020–2022 | Performance — Concurrent rendering, Suspense, automatic batching |
| 2023–2024+ | Server-first — RSC, Server Actions, zero-JS by default |
React's trajectory is moving the "work" from the client to the server, while making the client-side code that remains simpler to write and impossible to misuse. The React Compiler is the logical endpoint of this philosophy: the developer writes straightforward code, and the toolchain handles the rest.
Further Reading
- React Official Documentation — The canonical reference for all React APIs and patterns.
- React Blog — Release announcements and deep dives from the React team.
- MDN Web Docs: JavaScript — Foundations: Promises, modules, destructuring, and everything hooks rely on.
- MDN Web Docs: Web Components — The native platform alternative for context.
- Next.js Documentation — The primary framework for React Server Components in production.