React API Reference: Top-Level and Component APIs
Build a working reference to React's top-level and component-wrapping APIs — createElement, createRoot, hydrateRoot, Fragment, StrictMode, Suspense, lazy, and memo — covering purpose, syntax, common use, and pitfalls for each.
Beyond hooks, React exposes a set of top-level functions and special components that control how elements are created, how an app is mounted or hydrated into the DOM, and how a subtree's rendering behavior is modified (grouping, strict checks, loading states, code splitting, and memoization). This lesson is a consolidated reference to eight of those APIs, each with its purpose, syntax, a typical use case, a common mistake, and a note on which React version introduced it.
createElement, createRoot, and hydrateRoot
React.createElement(type, props, ...children) is the underlying function that JSX compiles down to — writing <div className="box">hi</div> is syntactic sugar for React.createElement('div', { className: 'box' }, 'hi'). You rarely call it directly in application code since JSX handles it, but understanding that JSX is just createElement calls explains why JSX expressions must return exactly one element (createElement returns one plain object) and why conditionally rendering JSX is really just conditionally choosing what value an expression evaluates to.
createRoot(domNode) (from react-dom/client, React 18+) creates a root React can render into, replacing the older ReactDOM.render(...) API. Calling .render(<App />) on the returned root mounts the app and opts the whole tree into React 18's concurrent features (automatic batching, transitions, concurrent rendering). A common mistake is calling createRoot more than once on the same DOM node across re-renders (for example, inside a function that runs on every hot reload) — createRoot should be called once per DOM container, and only .render() should be called again for a subsequent app update.
hydrateRoot(domNode, <App />) (React 18+, from react-dom/client) is used instead of createRoot when the DOM already contains server-rendered HTML markup that React should attach to and make interactive, rather than replace. It requires the initial client render to produce markup that matches the server-rendered HTML exactly — a mismatch (for example, rendering Date.now() differently on server and client) produces a hydration warning and React discarding and re-rendering the mismatched content on the client.
• createElement: the function JSX compiles to; rarely called directly. • createRoot(node): mounts a React app into a DOM node for client-side rendering (React 18+). • hydrateRoot(node, app): attaches React to existing server-rendered HTML instead of replacing it (React 18+).
Fragment, StrictMode, and Suspense
Fragment (written as <Fragment>...</Fragment> or the shorthand <>...</>) lets a component return multiple sibling elements without wrapping them in an extra DOM node like a <div>. This matters because a component function (and any single JSX expression) can only return one root node — Fragment satisfies that requirement without adding markup that could break CSS layouts relying on a specific parent-child DOM structure (like a <table> requiring <tr> as a direct child). The shorthand <> cannot take a key prop; use the explicit <Fragment key={...}> form when rendering a list of fragments.
StrictMode is a wrapper component (<StrictMode>...</StrictMode>) that enables extra development-only checks and warnings for its subtree — it has no effect in production builds. Among other things, it intentionally double-invokes component function bodies and certain effects in development to help surface side effects that aren't properly cleaned up, and it warns about legacy APIs. It renders no visible UI itself.
Suspense (<Suspense fallback={<Loading />}>...</Suspense>) lets a subtree 'suspend' — pause rendering and show a fallback — while something it depends on isn't ready yet, such as a lazily-loaded component or a promise read with use(). React shows the fallback until every suspending descendant resolves, then swaps in the real content. A common mistake is putting a Suspense boundary so high in the tree that any single slow-loading piece blocks the entire page's fallback, rather than placing boundaries closer to the components that actually need to suspend.
• Fragment (<>...</>): groups children without adding a DOM node. • StrictMode: development-only extra checks (double-invocation, deprecated API warnings); no production effect, no visible UI. • Suspense: shows a fallback while a descendant is not yet ready (lazy component, use() promise, etc.).
lazy and memo
lazy(() => import('./Chart')) defines a component that is code-split — its module is only fetched when it's first rendered, not included in the initial bundle. A component returned by lazy must be rendered inside a Suspense boundary, since the import itself is asynchronous and will cause the component to suspend the first time it's rendered while the module downloads. This is the main API for route-based or component-based code splitting in React.
memo(Component) wraps a component so React skips re-rendering it when its props are shallow-equal to the previous render's props (used from the parent's perspective — the parent still re-renders, but the memoized child's own re-render is skipped if nothing relevant changed). It's a targeted performance optimization for components that render often with unchanged props or that are expensive to re-render, not a default to apply everywhere — memoizing a cheap component adds the overhead of a props comparison without meaningful benefit, and passing a new inline object or function as a prop on every render defeats it since shallow equality will always see it as changed.
Both lazy and memo are top-level React.* functions rather than hooks and can be used with class or function components. lazy has existed since React 16.6; memo has existed since React 16.6 as well, well before the introduction of hooks-based memoization tools like useMemo and useCallback, which handle memoizing values and functions *within* a component rather than memoizing a whole component's render output.
• lazy(() => import(...)): code-splits a component; must be rendered inside Suspense. • memo(Component): skips re-rendering a component when its props are shallow-equal to last time.
createRoot: mounting a React app
createRoot is React 18+; React 17 and earlier used ReactDOM.render(<App />, container) instead. Only call createRoot once for a given DOM node — call root.render(...) again for subsequent updates, such as after a hot module reload guard.
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
if (!container) throw new Error('Root element not found');
const root = createRoot(container); // call ONCE per container
root.render(<App />);Code Examples
createRoot replaces whatever is in the container; hydrateRoot instead reuses the existing server-rendered DOM nodes and attaches event listeners and React's internal bookkeeping to them, as long as the client's first render produces matching markup.
hydrateRoot(container, <App />) Common mistake: Using createRoot on server-rendered markup discards the server HTML and re-renders everything from scratch on the client, losing the performance benefit of server rendering and causing a visible flash.
DescriptionItem uses the <> shorthand since no key is needed there; Glossary's map callback needs a key on each item, so it uses the explicit <Fragment key={...}> form instead, since <> cannot accept props. Expected result: Renders a valid <dl> with <dt>/<dd> pairs as direct children, with no extra wrapping <div> in the DOM.
// Client-only app (no server-rendered markup to attach to)
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')!).render(<App />);
// Server-rendered app being hydrated on the client
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root')!, <App />);function DescriptionItem({ term, detail }: { term: string; detail: string }) {
// <dt> and <dd> must be direct children of <dl> in valid HTML —
// a wrapping <div> here would break that structure.
return (
<>
<dt>{term}</dt>
<dd>{detail}</dd>
</>
);
}
function Glossary({ items }: { items: { term: string; detail: string }[] }) {
return (
<dl>
{items.map((item) => (
<Fragment key={item.term}>
<dt>{item.term}</dt>
<dd>{item.detail}</dd>
</Fragment>
))}
</dl>
);
}lazy + Suspense code-splitting flow
Suspense is required around any lazy() component because the dynamic import is asynchronous.
const Chart = lazy(() => import('./Chart'));
<Suspense fallback={<Spinner />}>
<Chart />
</Suspense>
First render:
React renders <Chart /> --> triggers import('./Chart') --> component suspends
Suspense catches the suspension --> shows <Spinner />
import finishes --> React re-renders --> real <Chart /> content replaces <Spinner />Important Rules
createRoot and hydrateRoot are React 18+ APIs, replacing the legacy ReactDOM.render and ReactDOM.hydrate.
A component wrapped in lazy() must be rendered somewhere inside a Suspense boundary, or React throws at render time when it suspends with nothing to catch it.
StrictMode's extra checks and double-invocations only run in development builds — never rely on double-invocation behavior as a way to run something twice in production.
memo only performs a shallow comparison of props by default — it will not help if you pass new object/array/function literals as props on every parent render.
Where this shows up
createRoot/hydrateRoot appear once, at the entry point of nearly every React app (index.tsx or the framework's equivalent). Fragment shows up constantly in everyday component code. StrictMode wraps the whole app during development in most starter templates. Suspense + lazy is the standard route-based code-splitting pattern, and memo appears selectively around components identified as re-render hotspots through profiling.
function App() { return ( <MemoizedChild config={{ theme: 'dark' }} onSave={() => save()} />); } const config = { theme: 'dark' } as const; // module scope or useMemo'd function App() { const handleSave = useCallback(() => save(), []); return <MemoizedChild config={config} onSave={handleSave} />; }
Passing a fresh object literal and a fresh arrow function as props on every render defeats memo's shallow prop comparison, since a new object/function reference is never shallow-equal to the previous render's — the memoized child re-renders every time regardless.
[multiple-choice] Which API must be used, instead of createRoot, to attach React to markup already produced by server-side rendering? A. lazy B. hydrateRoot C. memo D. StrictMode
B. hydrateRoot — hydrateRoot attaches React to existing server-rendered DOM nodes rather than discarding and re-rendering them, as long as the client's initial render output matches the server markup.
[true-false] A component wrapped in lazy() can be rendered anywhere without needing a Suspense boundary. A. True B. False
B. False — A lazy component suspends the first time it renders while its module downloads; without a Suspense boundary somewhere above it to catch that suspension, React has nothing to show in the meantime and errors.
[multiple-choice] Why might wrapping a component in memo() fail to prevent re-renders even though its props 'look the same'? A. memo only works on class components B. The parent is passing new object/array/function literals each render, which are never shallow-equal to the previous ones C. memo requires useMemo to be used inside the child D. memo is deprecated and does nothing
B. The parent is passing new object/array/function literals each render, which are never shallow-equal to the previous ones — memo's default comparison is shallow — it checks reference equality per prop. A freshly created object, array, or function passed as a prop is a new reference every render, so the comparison always reports a change even if the literal's contents are identical.
Write the entry-point code (as if in index.tsx) that mounts an <App /> component using createRoot, wraps it in <StrictMode>, and explain in one sentence why StrictMode has no effect once the app is built for production.
import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')!).render(<StrictMode><App /></StrictMode>); — StrictMode's double-invocation and extra warnings only run in React's development mode; production builds skip those checks entirely since they exist to catch bugs during development, not to change runtime behavior for end users.
StrictMode's checks are stripped or no-ops in production builds; it exists purely as a development-time diagnostic tool.