React 10 ⚛️ Setting Default Props Values
A component often needs a value that the caller may or may not provide. A button has a variant that defaults to primary. An avatar has a size that defaults to 40. A greeting has a salutation that defaults to “Hello.” Default prop values supply these fallbacks so that callers can omit props they do not care about, and the component still renders sensibly. Without defaults, every caller would have to provide every prop, and a missing prop would render undefined in the output.
React has two mechanisms for default props: the modern approach, which uses destructuring defaults in the function parameter, and the legacy approach, which uses Component.defaultProps. The destructuring approach is what the React documentation now recommends and what works correctly with function components, TypeScript, and React’s upcoming compiler. The defaultProps static property still exists for backward compatibility, particularly for class components, but it is deprecated for function components and behaves in ways that surprise people.
This chapter covers both mechanisms, when each applies, how they interact with undefined and null, the distinction between default values and fallback rendering, and the TypeScript patterns that make defaults type-safe.
Key point: Use destructuring defaults in the function parameter for function components: function C({ size = 40 }) {}. The defaultProps static property is legacy and deprecated for function components; it applies only when a prop is undefined, not when it is null, 0, false, or "".
Why default props exist
The omitted prop problem. A component with many props rarely receives all of them from every caller. Some props are essential and must be provided; others have a natural default that works in most cases. Defaults let callers provide only what they care about, and the component fills in the rest.
The undefined rendering problem. When a prop is not provided, its value is undefined. Rendering undefined produces nothing, which may be acceptable for optional content but is wrong for a required display value. A size prop that is undefined used in width={size} produces an invalid attribute. Defaults prevent this by supplying a real value.
The API design problem. Defaults are part of a component’s API. They document what happens when a prop is omitted, and they let the component define reasonable behavior without requiring the caller to know every detail. A Button that defaults to variant="primary" is easier to use than one that requires the variant every time.
The null-versus-undefined problem. JavaScript distinguishes undefined (not provided) from null (provided as no value). Defaults apply when a prop is undefined, but not when it is null. This distinction matters because a caller who passes null is explicitly saying “no value,” while a caller who omits the prop is saying “use the default.” Understanding this prevents the bug where a default does not apply because the caller passed null instead of omitting the prop.
The migration problem. The defaultProps mechanism predates function components. When function components became the default, a new mechanism was needed that worked with destructuring and TypeScript. The destructuring default is that mechanism, and it is the one to use in new code.
a. Destructuring defaults: the modern approach
The recommended way to set default prop values is a default in the destructuring pattern of the function parameter.
function Button({ label, variant = "primary", size = "medium" }) {
return (
<button className={`btn btn-${variant} btn-${size}`}>
{label}
</button>
);
}
When the caller omits variant or size, the defaults apply. When the caller provides them, the provided values are used.
<Button label="Save" />
// variant = "primary", size = "medium"
<Button label="Save" variant="danger" />
// variant = "danger", size = "medium"
Defaults can reference earlier parameters in the same destructuring pattern, but only in that order:
function Card({ width = 100, height = width * 2 }) {
return <div style={{ width, height }} />;
}
This works because width is bound before height is evaluated. Reversing the order would produce a reference error.
Defaults can also be functions, which are useful when the default value is expensive to compute or should be a new instance each render:
function List({ items = [], renderItem = (x) => x }) {
return <ul>{items.map(renderItem)}</ul>;
}
Using [] as a default creates a new array on each render when the prop is omitted. If this is a performance concern, the default can be defined outside the component:
const DEFAULT_ITEMS = [];
function List({ items = DEFAULT_ITEMS }) {
return <ul>{items.map((x) => <li key={x.id}>{x.name}</li>)}</ul>;
}
b. The defaultProps static property: legacy
Before function components, class components set defaults with a static property:
class Button extends React.Component {
static defaultProps = {
variant: "primary",
size: "medium",
};
render() {
return <button className={`btn btn-${this.props.variant}`}>{this.props.label}</button>;
}
}
For function components, the same property can be attached:
function Button({ label, variant, size }) {
return <button className={`btn btn-${variant} btn-${size}`}>{label}</button>;
}
Button.defaultProps = {
variant: "primary",
size: "medium",
};
This still works in current React versions, but it is deprecated. The React documentation recommends destructuring defaults instead. There are three reasons:
- The React compiler. The upcoming React compiler optimizes function components by analyzing their props. The
defaultPropsproperty is a runtime mutation of the function object, which the compiler cannot analyze as easily as a destructuring default. - TypeScript friction.
defaultPropsand TypeScript’s type inference for props interact awkwardly. The destructuring default is type-safe: the default value must match the prop’s type, and the prop becomes optional when a default is present. - Class component decline.
defaultPropsremains relevant for class components, which still use it. As class components are phased out, so is the need fordefaultProps.
A deprecation warning appears in React 18.3 and later when defaultProps is used on a function component. The fix is to move the defaults into the destructuring.
c. Undefined versus null
Defaults apply when a prop is undefined, which is what happens when the prop is omitted. They do not apply when a prop is null, 0, false, or "".
function Greeting({ name = "Guest" }) {
return <h1>Hello, {name}!</h1>;
}
<Greeting /> // name = "Guest"
<Greeting name={undefined} /> // name = "Guest"
<Greeting name={null} /> // name = null → renders "Hello, !"
<Greeting name="" /> // name = "" → renders "Hello, !"
<Greeting name="Alice" /> // name = "Alice"
The behavior is defined by JavaScript’s default parameter semantics, which apply only when the argument is undefined. This is usually what you want: undefined means “not provided,” and null means “provided as no value.”
But it can cause confusion. A caller who wants to clear a value may pass null, expecting the default to apply. It does not. If the component needs to treat null as “use the default,” it must handle that explicitly:
function Greeting({ name }) {
const displayName = name ?? "Guest";
return <h1>Hello, {displayName}!</h1>;
}
The nullish coalescing operator ?? applies the default when the value is null or undefined. This is different from ||, which applies the default for any falsy value, including 0, false, and "".
function Counter({ count = 0 }) {
return <span>{count}</span>;
}
<Counter count={0} /> // count = 0, renders "0"
<Counter count={null} /> // count = null, renders ""
<Counter /> // count = 0
The choice between = default, ??, and || depends on what the component should do when the prop is absent, null, or falsy. The destructuring default handles the omitted case. ?? handles both omitted and null. || handles all falsy values, which is usually not what is wanted for numbers and booleans.
d. Defaults for children and slots
The children prop has no default in the destructuring sense, but a component can render fallback content when children are absent.
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div>{children ?? <p>No content</p>}</div>
</div>
);
}
The ?? operator is appropriate here because children may be undefined when the component is used as a self-closing tag:
<Card title="Empty" /> // children is undefined → fallback renders
<Card title="Filled"><p>Hi</p></Card> // children is the element
A default in the destructuring also works:
function Card({ title, children = <p>No content</p> }) {
return (
<div className="card">
<h3>{title}</h3>
<div>{children}</div>
</div>
);
}
Both approaches are valid. The destructuring default is evaluated once per render, which is fine for a static fallback element.
e. TypeScript and default props
In TypeScript, props with defaults are usually marked optional, and the default value is checked against the prop’s type.
interface ButtonProps {
label: string;
variant?: "primary" | "secondary" | "danger";
size?: "small" | "medium" | "large";
disabled?: boolean;
}
function Button({
label,
variant = "primary",
size = "medium",
disabled = false,
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
>
{label}
</button>
);
}
The ? marks the prop optional. The default value must be assignable to the prop’s type. TypeScript infers that inside the function body, variant, size, and disabled have their non-undefined types because the defaults guarantee a value.
With defaultProps, the type inference was less reliable. TypeScript would infer the props type from the interface, then defaultProps would be checked separately, and the interaction was confusing. Destructuring defaults avoid that friction entirely.
For components that require the default to be applied to null as well as undefined, the type can include null:
interface GreetingProps {
name?: string | null;
}
function Greeting({ name }: GreetingProps) {
const displayName = name ?? "Guest";
return <h1>Hello, {displayName}!</h1>;
}
The ?? handles both cases, and the type allows null to be passed.
f. Defaults and prop drilling
Defaults interact with prop drilling: when a prop is passed through several layers, a default at the top means that intermediate components may receive undefined and pass it down.
function Grandparent() {
return <Parent />; // no theme prop
}
function Parent({ theme = "light" }) {
return <Child theme={theme} />; // passes "light"
}
function Child({ theme }) {
return <div className={`theme-${theme}`} />;
}
The default in Parent ensures that Child receives a value. If Parent did not have a default, theme would be undefined in Child. The rule: apply the default at the component that needs it, or at the highest component that has the information to compute it.
For deeply drilled props, context is often a better solution. A default in context is set at the provider level, and every consumer reads the resolved value:
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Child />
</ThemeContext.Provider>
);
}
function Child() {
const theme = useContext(ThemeContext);
return <div className={`theme-${theme}`} />;
}
If the provider is absent, the context’s default value ("light") applies. This is a different mechanism from prop defaults, but it solves the same problem for values that many components need.
g. Common mistakes with defaults
Passing null instead of omitting. A caller who wants the default must omit the prop or pass undefined. Passing null disables the default.
<Button variant={null} /> // variant = null, not "primary"
<Button variant={undefined} /> // variant = "primary"
<Button /> // variant = "primary"
Using || where ?? is intended. || treats 0, false, and "" as falsy, so a default applies to them. For a count or a boolean, this is a bug.
function Counter({ count }) {
return <span>{count || 0}</span>; // renders 0 when count is false or ""
}
The ?? operator applies only to null and undefined:
function Counter({ count }) {
return <span>{count ?? 0}</span>; // renders 0 only when count is null/undefined
}
Defining default objects or arrays inline. A default of {} or [] creates a new instance on every render, which can cause unnecessary re-renders in memoized children. Define the default outside the component if identity matters.
const EMPTY_ARRAY = [];
function List({ items = EMPTY_ARRAY }) { /* ... */ }
Assuming defaultProps works with the React compiler. The React compiler, when it ships, analyzes function components statically. defaultProps is a runtime mutation that the compiler may not optimize for. Destructuring defaults are the forward-compatible choice.
Complete Example Session
// ============================================
// PART 1: BASIC DESTRUCTURING DEFAULT
// ============================================
function Greeting({ name = "Guest" }) {
return <h1>Hello, {name}!</h1>;
}
<Greeting /> // Hello, Guest!
<Greeting name="Alice" /> // Hello, Alice!
// ============================================
// PART 2: MULTIPLE DEFAULTS
// ============================================
function Button({ label, variant = "primary", size = "medium" }) {
return (
<button className={`btn btn-${variant} btn-${size}`}>{label}</button>
);
}
<Button label="Save" />
// variant = "primary", size = "medium"
// ============================================
// PART 3: DEFAULTS WITH UNION TYPES
// ============================================
function Alert({ type = "info", message = "No message" }) {
return <div className={`alert alert-${type}`}>{message}</div>;
}
<Alert />
// type = "info", message = "No message"
// ============================================
// PART 4: DEFAULTS WITH OBJECTS AND ARRAYS
// ============================================
const EMPTY_ITEMS = [];
function List({ items = EMPTY_ITEMS }) {
return <ul>{items.map((i) => <li key={i.id}>{i.name}</li>)}</ul>;
}
// ============================================
// PART 5: UNDEFINED VS NULL
// ============================================
function Card({ title = "Untitled" }) {
return <h2>{title}</h2>;
}
<Card /> // "Untitled"
<Card title={undefined} /> // "Untitled"
<Card title={null} /> // renders nothing (title is null)
<Card title="" /> // renders empty string
// ============================================
// PART 6: NULLISH COALESCING FOR NULL
// ============================================
function Card({ title }) {
return <h2>{title ?? "Untitled"}</h2>;
}
<Card title={null} /> // "Untitled"
<Card title={undefined} /> // "Untitled"
<Card title="" /> // ""
// ============================================
// PART 7: FALSY VALUES AND ||
// ============================================
function Counter({ count }) {
return <span>{count || 0}</span>;
}
<Counter count={0} /> // renders "0" (0 is falsy, so || 0 applies)
<Counter count={5} /> // renders "5"
<Counter count={false} /> // renders "0"
// ============================================
// PART 8: LEGACY defaultProps
// ============================================
function LegacyButton({ label, variant }) {
return <button className={`btn-${variant}`}>{label}</button>;
}
LegacyButton.defaultProps = {
variant: "primary",
};
// Works, but shows a deprecation warning in React 18.3+
// ============================================
// PART 9: TYPESCRIPT DEFAULTS
// ============================================
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
size?: "small" | "medium" | "large";
disabled?: boolean;
}
function Button({
label,
variant = "primary",
size = "medium",
disabled = false,
}: ButtonProps) {
return (
<button className={`btn btn-${variant} btn-${size}`} disabled={disabled}>
{label}
</button>
);
}
// ============================================
// PART 10: CHILDREN FALLBACK
// ============================================
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div>{children ?? <p>No content</p>}</div>
</div>
);
}
<Card title="Empty" /> // renders "No content"
<Card title="Filled"><p>Hi</p></Card> // renders "Hi"
These ten parts cover the range of default prop patterns: basic defaults, multiple defaults, union types, objects and arrays, the undefined/null distinction, nullish coalescing, the falsy pitfall, legacy defaultProps, TypeScript defaults, and children fallback. Each pattern is idiomatic modern React.
Quick Reference
Setting Defaults
| Approach | Syntax | Status |
|---|---|---|
| Destructuring default | function C({ x = 1 }) {} | Recommended |
| Nullish coalescing | const v = x ?? 1 | For null and undefined |
| Logical OR | const v = x || 1 | For all falsy values |
defaultProps | C.defaultProps = { x: 1 } | Legacy for function components |
Undefined vs Null vs Falsy
| Value | x = 1 | x ?? 1 | x || 1 |
|---|---|---|---|
undefined | 1 | 1 | 1 |
null | null | 1 | 1 |
0 | 0 | 0 | 1 |
false | false | false | 1 |
"" | “” | “” | 1 |
5 | 5 | 5 | 5 |
Defaults by Prop Type
| Prop Type | Recommended Default |
|---|---|
| String | { name = "Guest" } |
| Number | { count = 0 } |
| Boolean | { disabled = false } |
| Array | { items = EMPTY_ARRAY } |
| Object | { options = EMPTY_OBJECT } |
| Function | { onClick = noop } |
| Children | { children ?? fallback } |
TypeScript Patterns
| Pattern | Type |
|---|---|
| Optional prop with default | variant?: "primary" | "secondary" |
| Prop allowing null | title?: string | null |
| Default must match type | variant = "primary" (checked) |
Best Practices
✅ Do This:
function Button({ variant = "primary" }) { ... } // Destructuring default
function Card({ title }) { return <h2>{title ?? "Untitled"}</h2>; } // ?? for null
const EMPTY = [];
function List({ items = EMPTY }) { ... } // Stable default reference
interface Props { variant?: "primary" | "secondary"; } // Optional in TypeScript
❌ Don’t Do This:
Button.defaultProps = { variant: "primary" }; // ❌ Legacy
function Counter({ count }) { return <span>{count || 0}</span>; } // ❌ || hides 0
function List({ items = [] }) { ... } // ❌ New array each render
<Button variant={null} /> // ❌ Disables default
function C({ x = 1 }) { x = 2; } // ❌ Mutating prop
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Default not applied | Caller passed null | Omit prop or use ?? |
| Zero rendered as default | || treats 0 as falsy | Use ?? for numbers and booleans |
| New array each render | Inline default [] | Define default outside component |
| Deprecation warning | defaultProps on function component | Move to destructuring |
| TypeScript error | Prop required but default provided | Mark prop optional with ? |
| Default reference error | Default references later parameter | Order parameters correctly |
| Children default not applied | Child passed but falsy | Use ?? for children fallback |
Real-World Examples
1. Button Variant Default
function Button({ label, variant = "primary" }) {
return <button className={`btn-${variant}`}>{label}</button>;
}
2. Size Default
function Avatar({ src, size = 40 }) {
return <img src={src} width={size} height={size} />;
}
3. Boolean Default
function Input({ value, disabled = false }) {
return <input value={value} disabled={disabled} />;
}
4. Array Default
const NO_TAGS = [];
function TagList({ tags = NO_TAGS }) {
return <ul>{tags.map((t) => <li key={t}>{t}</li>)}</ul>;
}
5. Object Default
const DEFAULT_OPTIONS = { theme: "light" };
function Panel({ options = DEFAULT_OPTIONS }) {
return <div className={options.theme} />;
}
6. Callback Default
const noop = () => {};
function Button({ onClick = noop, children }) {
return <button onClick={onClick}>{children}</button>;
}
7. Nullish Coalescing for Null
function Card({ title }) {
return <h2>{title ?? "Untitled"}</h2>;
}
8. Children Fallback
function Card({ children }) {
return <div>{children ?? <p>Empty</p>}</div>;
}
9. TypeScript Optional Prop
interface Props {
variant?: "primary" | "secondary";
size?: number;
}
function Button({ variant = "primary", size = 14 }: Props) { ... }
10. Context Default
const ThemeContext = createContext("light");
function Child() {
const theme = useContext(ThemeContext); // "light" if no provider
return <div className={theme} />;
}
Visual
Destructuring Defaults Apply Only to undefined
┌──────────────────────────────────────────────────────────────┐
│ function C({ x = "default" }) {} │
│ │
│ Caller passes │ x value │ Default applied? │
│ ─────────────────┼────────────┼──────────────────────────── │
│ (omitted) │ "default" │ Yes │
│ undefined │ "default" │ Yes │
│ null │ null │ No │
│ 0 │ 0 │ No │
│ false │ false │ No │
│ "" │ "" │ No │
│ "value" │ "value" │ No │
│ │
│ The default applies only when the value is undefined. │
│ For null as well, use ?? in the body. │
└──────────────────────────────────────────────────────────────┘
Defaults vs Fallbacks
┌──────────────────────────────────────────────────────────────┐
│ THREE APPROACHES, DIFFERENT RESULTS │
│ │
│ Destructuring default: { x = 1 } │
│ └── undefined → 1; null → null; 0 → 0 │
│ │
│ Nullish coalescing: x ?? 1 │
│ └── undefined → 1; null → 1; 0 → 0 │
│ │
│ Logical OR: x || 1 │
│ └── undefined → 1; null → 1; 0 → 1; "" → 1; false → 1 │
│ │
│ Choose based on which values should trigger the default: │
│ - Omitted only: destructuring default │
│ - Omitted or null: ?? │
│ - Any falsy: || (rarely correct for numbers/booleans) │
└──────────────────────────────────────────────────────────────┘
Legacy defaultProps vs Modern Defaults
┌──────────────────────────────────────────────────────────────┐
│ defaultProps (LEGACY) DESTRUCTURING (MODERN) │
│ │
│ function Button({ function Button({ │
│ label, variant label, │
│ }) { variant = "primary" │
│ return <button }) { │
│ className={`btn- return <button │
│ ${variant}`}> className={`btn- │
│ {label} ${variant}`}> │
│ </button>; {label} │
│ } </button>; │
│ } │
│ Button.defaultProps = { │
│ variant: "primary" // Default is inline, │
│ }; // visible in the signature │
│ │
│ - Runtime mutation - Static, analyzable │
│ - Deprecation warning - Recommended │
│ - TypeScript friction - Type-safe │
│ - Works for class components - Function components only │
└──────────────────────────────────────────────────────────────┘
Defaults in a Prop Chain
┌──────────────────────────────────────────────────────────────┐
│ WHERE TO APPLY THE DEFAULT │
│ │
│ App │
│ └── <Layout /> (no theme prop) │
│ │ │
│ ▼ │
│ Layout({ theme = "light" }) │
│ └── <Sidebar theme={theme} /> (resolves to "light") │
│ │ │
│ ▼ │
│ Sidebar({ theme }) │
│ └── <div className={theme} /> (receives "light") │
│ │
│ Apply the default at the highest component that has the │
│ information, so that children always receive a resolved │
│ value. Alternatively, use context for values many │
│ components need. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Modern approach | Destructuring default in the parameter |
| Legacy approach | Component.defaultProps (deprecated for functions) |
| Applies to | undefined only (omitted or explicit undefined) |
| Does not apply to | null, 0, false, "" |
| For null as well | Use ?? in the body |
| For all falsy | Use || (rarely correct) |
| Arrays and objects | Define default reference outside component |
| Children fallback | children ?? <Fallback /> |
| TypeScript | Mark prop optional with ? |
| Context default | createContext(defaultValue) |
| React compiler | Prefers destructuring defaults |
Key takeaways:
- Use destructuring defaults for function components.
function Button({ variant = "primary" })is the modern, recommended approach. - Defaults apply only when the value is
undefined. Passingnull,0,false, or""disables the default because those are values, not absences. - Use
??whennullshould also trigger the default. The nullish coalescing operator applies the fallback for bothnullandundefined. - Avoid
||for numeric and boolean defaults.||treats0andfalseas falsy, so the default applies when the caller passed a legitimate value. defaultPropsis legacy. It still works for class components and, with a deprecation warning, for function components. New code should use destructuring defaults.- Define array and object defaults outside the component. An inline
[]or{}creates a new instance on every render, which can cause unnecessary re-renders. - TypeScript marks props with defaults as optional. The
?in the interface and the default in the destructuring work together: TypeScript ensures the default matches the type, and the prop is optional at the call site. - Apply defaults at the right level. A default at the top of a prop chain ensures every descendant receives a resolved value. For values many components need, context provides a cleaner alternative.
Remember: Default prop values are part of a component’s API. They define what happens when a caller omits a prop, and they let components have sensible behavior without requiring every caller to provide every value. The modern mechanism is the destructuring default, which is static, type-safe, and compatible with the React compiler. The legacy defaultProps property still exists for backward compatibility, but new code should not use it for function components. The subtle part is the distinction between undefined (not provided) and null (provided as no value): the destructuring default handles the first, ?? handles both, and || handles all falsy values, which is usually not what is wanted for numbers and booleans. Getting this right prevents the bug where a default does not apply because the caller passed the wrong kind of empty value.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!