Dynamic React Components: The Four Shapes, and Which One You Need
Most guides show a string-keyed registry. This codebase's 92 components use three other shapes instead — a variant prop, selection state, and data that carries its own JSX.
A "dynamic React component" almost always means one of four different things, and most tutorials only show the flashiest one — a string-keyed lookup table plus next/dynamic. This storefront runs 92 components and has zero of those. What it has instead: a prop that switches which markup a component returns, state that decides which of an array's items renders, and data files that carry a component's shape as a value rather than a name. Here is all four, with the real code, and the one place a truly dynamic import earns its keep.
The short answer
"Dynamic" React component usually means one of: (1) a component whose own JSX branches on a prop, (2) a parent picking which pre-known item to render from state, (3) data that stores a component reference or JSX directly instead of a string key, or (4) a genuine runtime import() when the set of possible components isn't known until the page loads. Reach for the string-key-plus-registry pattern only for the fourth case — it's the least common one, and applying it to the first three adds a lookup table around a decision a ternary already made.
Four shapes, one real codebase
| Pattern | What decides the output | When it's the right tool | Where this repo uses it |
|---|---|---|---|
| Prop-driven branch | A single prop, checked once | Two known layouts of the same component | DownloadMenu's variant prop |
| State-driven selection | useState, re-rendering on change | Picking one of several already-loaded items | EditionPicker's edition state |
| Data carries the element | The array item itself is a ReactNode | Content that never changes shape, only which item runs | src/data/*.tsx icon fields |
| Registry + dynamic import | A string key resolved at runtime | The component set isn't fixed at build time | Not used here — see below |
1. A prop switches which JSX a component returns
DownloadMenu is the clearest example: one component, two visual shapes, chosen by a variant prop rather than two separate components with duplicated logic.
// src/components/molecules/DownloadMenu.tsx
export default function DownloadMenu({
options,
variant = "button",
productName,
className = "",
}: {
options: DownloadOption[];
variant?: "icon" | "button";
productName: string;
className?: string;
}) {
const isIcon = variant === "icon";
return (
<button
aria-label={isIcon ? `Download ${productName}` : undefined}
className={
isIcon
? "inline-flex h-9 w-9 items-center justify-center rounded-full ..."
: "inline-flex items-center justify-center gap-1.5 rounded-lg ..."
}
>
{isIcon ? (
<Download className="h-5 w-5" aria-hidden="true" />
) : (
<>
<Download className="h-4 w-4" aria-hidden="true" />
Download
<ChevronDown className="h-4 w-4" aria-hidden="true" />
</>
)}
</button>
);
}
variant picks between an icon-only round button on a card's cover art and a labelled pill inside the preview modal's Download button — the same component, same state machine (open/hover/pinned), same accessibility wiring, rendering two shapes. This is what most "dynamic component" searches actually mean, and it needs nothing beyond a ternary and a prop.
2. State picks which pre-loaded item to show
EditionPicker is the second shape: several editions already exist as data, and useState decides which one is currently on screen.
// src/components/molecules/EditionPicker.tsx
const [selected, setSelected] = useState(
product.editions.find((e) => e.status === "ready") ?? product.editions[0],
);
Clicking a framework chip calls setSelected, and the preview button, price and gallery all re-render against the new edition. No component is swapped — the same EditionPicker tree stays mounted the whole time, and only the data it reads changes. This is the pattern most guides reach for a switch statement or a component map to solve, when the actual requirement is "re-render this subtree with different data."
3. The data file carries the element, not a name for it
The least-discussed shape is also the simplest: instead of storing a string like "CheckIcon" and looking it up in a map, this codebase's section data stores the JSX directly.
// src/data/features.tsx
export type FeatureItem = {
icon: ReactNode;
title: string;
description: string;
};
export const featureCards: FeatureItem[] = [
{
icon: <svg className="h-12 w-12" /* … */>{/* … */}</svg>,
title: "Powered by Tailwind CSS",
description: "…",
},
// …
];
The organism that renders this just writes {item.icon} — there is no iconMap[item.iconName] anywhere in this codebase, because the icon is the data, typed as ReactNode. This is why 12 of the 15 files in src/data are .tsx rather than .ts: the moment an array's items need to embed markup, "dynamic rendering" stops being a lookup problem and becomes a plain .map().
4. Every slide stays mounted — the deliberate non-example
TemplateGallery, the product page's screenshot carousel, is worth including precisely because it looks like a job for conditional mounting and isn't built that way:
// src/components/molecules/TemplateGallery.tsx
// Every slide stays mounted in a translated track rather than swapping one
// <img>: crawlers still see all of the product's imagery, and moving between
// slides costs a transform instead of a network request.
const [index, setIndex] = useState(0);
A naive "dynamic component" instinct here would render only the active slide and unmount the rest. This codebase renders all of them, always, and moves a CSS translateX to bring one into view. The reason is SEO, not performance: an unmounted slide is invisible to a crawler reading the DOM, and a product page's screenshots are exactly the content Google Images and AI answer engines pull from. Sometimes the "dynamic" behaviour a user perceives should not correspond to any component actually mounting or unmounting at all.
Where the fourth pattern — the registry — would actually apply
The string-key-plus-next/dynamic() pattern is real and has a real job: rendering a component whose identity, not just its data, isn't known until runtime — a CMS-driven page where each block names its own renderer, or a plugin system. grep -rn "next/dynamic" src/ on this repository returns nothing, because nothing here has that shape: every page's component tree is fixed at build time, even the ones built from data arrays. Reaching for a registry when a ternary or a .find() would do adds a layer of indirection — and a runtime failure mode (an unmatched key) — that a closed set of four "shapes" doesn't have.
Mistakes and how they show up
| Symptom | Cause | Fix |
|---|---|---|
| A component re-mounts (loses state, replays animations) when only a prop changed | Rendering two separate components behind a conditional instead of one component with a variant prop | Collapse to one component; branch inside its JSX, as DownloadMenu does |
iconMap[item.iconName] throws undefined is not a function | A string key drifted from the map's keys | Store the element itself in the data, typed ReactNode, and drop the map entirely |
next/dynamic import used for a fixed, small set of known components | Reaching for the runtime-registry pattern by habit | A plain object of components, or even a switch, resolves at build time and gives better type errors |
| A carousel's later slides are missing from the rendered HTML | Only the active slide is mounted | Mount every slide in a translated track (see TemplateGallery) if the content needs to be crawlable |
| Selecting an item causes a visible flash before content appears | The selected item is fetched on click instead of already being in state | Load the full set once, then switch which item is read from state — as EditionPicker does across already-fetched editions |
Frequently asked questions
What's the difference between a dynamic component and a conditional render?
None, most of the time — {condition ? <A /> : <B />} is dynamic rendering. "Dynamic component" only needs a separate name when the set of possible components isn't fixed at build time, which is rarer than tutorials suggest.
Should I use next/dynamic for a component that's just hidden sometimes?
No. next/dynamic exists for code-splitting a component that might never be needed on a given page (a heavy chart library, a modal). A component that's simply conditionally shown should use a normal conditional — no bundle-splitting benefit exists if the component always ships anyway.
How do I render a list of different component types from data?
Store the component or element as a typed field on each data item (icon: ReactNode, as this codebase's src/data/*.tsx files do) rather than a string name resolved through a lookup object. It removes a whole class of "key not found" bugs and gives you a compiler error instead of a runtime one if a shape is missing.
Does switching between rendered items with state cause a re-mount?
Only if the JSX tree's structure changes at the point of the switch. EditionPicker keeps the same component mounted and only changes which edition object it reads, so there's no re-mount, no lost scroll position, and no replayed CSS transitions — which is usually what "dynamic" is supposed to feel like to a user.
Templates in this post
ASoc Folio, ASoc Forge and ASoc Frame all ship these same molecules — DownloadMenu's variant prop and EditionPicker's edition state included — so the patterns above are the actual code you get, not a simplified example.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
