Default & Required Props
Not every prop is supplied on every render. A component often has optional inputs that should fall back to a sensible value when omitted, and required inputs that must always be present for it to work. Knowing how to express both — cleanly and safely — prevents undefined from leaking into your UI and makes a component’s contract obvious to anyone using it. In modern React, destructuring defaults handle the optional case, and TypeScript handles the required case.
Default values with destructuring
The idiomatic way to give a prop a fallback in modern React is to assign a default directly in the destructuring pattern of the function signature. If the caller omits the prop (or passes undefined), the default is used instead.
function Button({ label = "Submit", variant = "primary", disabled = false }) {
return (
<button className={`btn btn-${variant}`} disabled={disabled}>
{label}
</button>
);
}
function App() {
return (
<div>
<Button />
<Button label="Cancel" variant="secondary" />
</div>
);
}
Output:
[Submit] (primary, enabled)
[Cancel] (secondary, enabled)
The first Button renders with all three defaults; the second overrides two of them. This approach keeps the fallback values right next to the props they belong to, so the function signature reads as living documentation of the component’s API.
Defaults only fire for
undefined, not fornull. Passinglabel={null}yieldsnull, not"Submit". Ifnullis a possible incoming value and you want the fallback, normalize it explicitly:const text = label ?? "Submit";.
The legacy defaultProps
Before destructuring defaults became standard, React supported a static defaultProps property on the component. You will still encounter it in older codebases and class components.
function Avatar({ size, alt }) {
return <img src="/avatar.png" width={size} alt={alt} />;
}
Avatar.defaultProps = {
size: 48,
alt: "User avatar",
};
For function components, defaultProps is deprecated and React logs a warning recommending default parameters instead. It still works for class components, but new code should not rely on it for function components.
// Class component — defaultProps is still the supported mechanism here
class Tag extends React.Component {
render() {
return <span className="tag">{this.props.text}</span>;
}
}
Tag.defaultProps = { text: "untagged" };
The two approaches compare as follows:
| Aspect | Destructuring defaults | defaultProps |
|---|---|---|
| Function components | Recommended | Deprecated (warns) |
| Class components | Not available | Supported |
| Where defaults live | In the signature | In a separate static object |
| Type inference (TS) | Inferred automatically | Needs extra typing |
| Applies to | undefined values | undefined values |
Marking props as required
JavaScript itself cannot enforce that a prop is provided — omitting one simply yields undefined. TypeScript is the modern way to make a prop required and catch the mistake at compile time. In a props interface, an optional prop is marked with ?; everything else is required.
interface UserCardProps {
name: string; // required
email: string; // required
role?: string; // optional
}
function UserCard({ name, email, role = "member" }: UserCardProps) {
return (
<article>
<h3>{name}</h3>
<p>{email}</p>
<span>Role: {role}</span>
</article>
);
}
// ✅ Valid
<UserCard name="Ada" email="[email protected]" />;
// ❌ Compile error: Property 'email' is missing
<UserCard name="Ada" />;
Note how role is both optional (? in the type) and given a default (= "member") in the signature — the two work together so that inside the component role is always a defined string. For projects without TypeScript, the legacy prop-types library performs the same check at runtime in development; see the related page on prop types.
Handling missing props safely
Even with types and defaults, you should write components that degrade gracefully when data is incomplete — especially for objects and arrays, where a missing nested value is easy to miss.
function Profile({ user = {}, tags = [] }) {
const displayName = user.name ?? "Anonymous";
return (
<section>
<h2>{displayName}</h2>
{tags.length > 0 ? (
<ul>{tags.map((t) => <li key={t}>{t}</li>)}</ul>
) : (
<p>No tags</p>
)}
</section>
);
}
<Profile />;
Output:
Anonymous
No tags
Three techniques are doing the work here: default parameters give user and tags safe empty containers, optional chaining (?.) and nullish coalescing (??) guard against missing nested fields, and a length check avoids rendering an empty list. Together they ensure the component never crashes on undefined.
A common gotcha: defaulting an object or array prop to a new literal (
= {}or= []) creates a fresh reference on every render. That is fine for reads, but if you pass it into auseEffectdependency array or a memoized child, it can trigger unwanted re-runs. Hoist truly constant defaults to module scope when identity matters.
Best Practices
- Prefer destructuring defaults over
defaultPropsfor all function components. - Keep defaults beside their props in the signature so the API is self-documenting.
- Use TypeScript (or
prop-types) to make required props explicit and catch omissions early. - Mark a prop optional with
?and pair it with a default so its value is neverundefinedinside the component. - Remember defaults apply only to
undefined; use??whennullshould also fall back. - Guard object and array props with safe defaults plus optional chaining before reading nested fields.
- Hoist constant object/array defaults to module scope when referential identity affects effects or memoization.