React: Passing Props, the Three Shapes This Codebase Actually Uses
Explicit named props, a full data-object spread, and forwarding the rest — 7 real spread call sites and Button's own default-parameter signature, from a 92-component codebase.
Passing props in React means writing them onto JSX like HTML attributes — <FeatureCard title={t} /> — and reading them back with a destructured function parameter. This storefront's 92 components almost never write that call by hand: 7 organisms spread a typed data object straight into a molecule with {...item}, and props never travel more than one layer before landing.
The short answer
Pass a prop by writing name={value} in JSX, and receive it by destructuring the function's single parameter: function Avatar({ name, size }). Values can be strings, numbers, objects, arrays, functions, or JSX itself. Props flow one direction only, parent to child, and are read-only on the receiving end — a child that needs to change what it received asks the parent to pass something different, it never mutates the prop.
Passing one prop at a time
The textbook form is explicit, one attribute per value:
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}</h1>;
}
<Greeting name="Alex" />;
This codebase uses that exact shape whenever the value passed down is computed by the parent rather than lifted verbatim from a data file. TemplateDetail, the organism behind every product page, is the clearest example:
<TemplateGallery images={galleryImages} name={product.name} />
galleryImages isn't a raw field on the catalog entry — it's resolved first (falling back to a single framed screenshot for a product with one image), then handed down as a plain prop. Explicit, named props are the right call whenever the parent is doing work before the value reaches the child.
Passing a whole object at once: the pattern used 7 times here
Most of this codebase's organisms don't compute anything — they map a typed array straight from src/data onto a molecule, and there's no reason to unpack it field by field first. Features.tsx is representative:
import FeatureCard from "@/components/molecules/FeatureCard";
import { featureCards } from "@/data/features";
export default function Features() {
return (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:gap-7.5">
{featureCards.map((c) => (
<FeatureCard key={c.title} {...c} />
))}
</div>
);
}
And the molecule on the receiving end destructures straight against the data file's own type, rather than declaring a separate props interface that would just have to be kept in sync with it:
import type { FeatureItem } from "@/data/features";
export default function FeatureCard({ icon, title, description }: FeatureItem) {
return (
<div className="rounded-3xl border border-stroke-secondary bg-gray-50 p-1">
<div className="mb-7.5 text-primary">{icon}</div>
<h3 className="mb-4 text-xl font-semibold">{title}</h3>
<p className="text-base text-text-color-secondary">{description}</p>
</div>
);
}
Grepping this codebase's organisms for {... — the spread-props call — turns up exactly 7 call sites across 6 files: FeatureTabs (once), Features (twice — FeatureCard and FeaturePill), Footer, Hero, Plugins and TechStack (once each). Every one of them is an organism handing a typed data item to its matching molecule verbatim. TechStackCard, PluginCard, TechBadge, FooterLinkColumn and the rest of that list all repeat the identical shape: a data file exports a typed array, the organism maps it, the molecule's function signature IS the array's item type. There's no props interface written twice — once in the data file, once in the component — because there's only one type, imported.
Explicit props vs. spread: when this codebase uses which
| Explicit named props | Spread ({...item}) | |
|---|---|---|
| When | The parent computes or derives the value | The parent has a data-file item already shaped like the props |
| Type source | An inline object type on the function parameter | The data file's own exported type (FeatureItem, TechStackItem, …) |
| Used in this repo | TemplateGallery, most molecules with client-side state | 7 organism → molecule pairs mapping over src/data/*.tsx |
| Risk | None specific — this is the default case | Passes every field whether the molecule reads it or not; fine when the type is the props type by design, wrong once the data file grows fields the molecule shouldn't render |
Default values and forwarding the rest
A prop doesn't have to be required, and it doesn't have to be read as a single named value either. Button, the atom every CTA on this site renders through, does both:
export default function Button({
variant = "primary",
href = "#",
external = false,
disabled = false,
className = "",
children,
...rest
}: {
variant?: ButtonVariant;
href?: string;
external?: boolean;
disabled?: boolean;
className?: string;
children: ReactNode;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const classes = `${base} ${variants[variant]} ${className}`.trim();
// ...
}
Two things worth calling out that a beginner tutorial on "passing props" usually skips. First, variant = "primary" and the other defaults are plain JavaScript default parameters on the destructured object — there's no separate defaultProps static property to declare, and every optional field in the type below is marked ? to match. Second, ...rest collects every prop the caller passed that Button didn't explicitly name, and it's spread onto the rendered element via React.AnchorHTMLAttributes<HTMLAnchorElement> — so a call site can pass aria-label, rel, or onClick straight through without Button needing to know about them individually. That's a third pattern alongside explicit-named and full-object-spread: forward what you don't recognize, useful specifically for a low-level atom wrapping a native element, where the whole point is not needing to re-declare HTML's own attribute list.
className, in that same signature, is a fourth micro-pattern worth naming: it defaults to an empty string and gets concatenated onto the variant's own classes rather than replacing them (`${base} ${variants[variant]} ${className}`) — "extend, don't override" is the convention every atom in this codebase follows for style overrides, and it only works because the prop is a string being appended, not an object being merged.
Why prop drilling never becomes a problem here
The usual objection to passing props is drilling — a value threaded through three or four components that don't use it, just to reach the one that does. This codebase's Atomic Design layering makes that structurally hard to do by accident: a page renders a template, a template renders organisms with zero props (HomeTemplate takes none and passes none — it only orders which organism renders where), and each organism owns its own data file and passes straight to the molecules and atoms one level below it. Props never cross the template layer at all, because the template doesn't hold any. The deepest a prop travels here is one hop: organism to molecule, or molecule to atom (className overrides, mostly). There's no fourth component in the middle that has to forward something it never reads, which is the actual condition that makes reaching for Context or a state library worth the added indirection — and it's a condition this codebase's own layering rule was written to avoid before it started.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Cannot read properties of undefined reading a prop | Prop name typo, or the parent forgot to pass it | Check the JSX call site's attribute name against the child's destructured names exactly |
| A prop update doesn't re-render the child | Passing a new object/array literal every render defeats React.memo, or the value genuinely didn't change | Confirm the parent's state actually changed; memoize the value if it's an expensive derivation |
Spreading {...item} passes fields the child doesn't want | The data file's type has grown fields the molecule shouldn't render (e.g. an internal-only flag) | Either accept the extra field is harmless (unused destructured props are ignored) or narrow the type at the call site |
| TypeScript complains a required prop is missing | The destructured type has no ? on a field the parent doesn't always have | Make the field optional (className?: string) only if the child can genuinely handle its absence |
| A deeply nested child needs a prop three components don't use | Classic prop drilling | Reach for children-as-composition first (pass the finished JSX down, not the raw data), then Context only if the value is truly cross-cutting |
Frequently asked questions
What's the difference between props and state? Props are passed in from a parent and are read-only from the child's side; state is local data a component owns and can update itself. A component receives props but manages state.
Can I pass a function as a prop in React?
Yes — it's how a child reports events upward, since props only flow one direction. onClick, onChange and this codebase's own onCtaClick-style callbacks are all functions passed as ordinary props.
Do I need PropTypes if I'm using TypeScript?
No. TypeScript's compile-time prop types make PropTypes's runtime checks redundant for anything written in .tsx; keeping both is duplicated maintenance for the same guarantee.
Is spreading props ({...props}) always fine to do?
It's fine when the spread source is exactly what the child's type expects — this codebase's 7 data-file spreads are all that case, because the molecule's props type IS the data file's item type. Spreading a bigger object "just in case" passes fields silently and makes it harder to see what a component actually depends on.
Where to take this next
This is the general case; Pass a component as a prop covers the narrower, specific pattern of handing a whole JSX element down as a prop's value rather than a string or number, which this codebase uses 35 times for its icon data. Atomic Design in Next.js is the layering rule that keeps props from ever needing to drill more than one hop in the first place.
Templates in this post
ASoc Quill, ASoc Rally and ASoc Rank are built on this same organism-to-molecule props pattern — typed data files, one-hop props, nothing to memoize by hand.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
