skip to content

React 19 removed support for defaultProps on function components. How do you give a function component's props default values now, and what is left of defaultProps?

level: middleimportance: should knowfreq 45%

answer

  1. defaults live in the parameter list now
  2. only undefined triggers a default
  3. zero and null are real values
  4. classes kept the old mechanism
  5. object literal defaults are new each render

basics

~10 s

Use JavaScript default parameter values in the destructuring of the props argument, such as function Button({ size = 'medium' }). React 19 ignores defaultProps on function components; it is still honoured on class components.

solid answer

~50 s

For function components you use plain JavaScript defaults in the parameter destructuring: `function Button({ size = 'medium', variant = 'primary' }) { ... }`. React 19 removed `defaultProps` handling for function components, so assigning `Button.defaultProps = { size: 'medium' }` no longer fills anything in — the prop simply arrives as `undefined`. `defaultProps` still works on class components, which is why it has not disappeared from the library entirely. The behavioural difference worth naming is the trigger: `defaultProps` applied whenever the prop was `undefined`, and destructuring defaults do exactly the same — they fire on `undefined` but not on `null`, `0`, `''` or `false`. So a caller passing `count={0}` keeps zero, while a caller passing `count={null}` gets `null`, not the default. If you want a null to fall back too, you need an explicit `??` in the body.

code

javascript · 13 lines
javascript
const EMPTY_ITEMS = [];

function Badge({ count = 5, items = EMPTY_ITEMS, ...rest }) {
  return (
    <span {...rest}>
      {count} / {items.length}
    </span>
  );
}

// <Badge />            -> 5
// <Badge count={0} />  -> 0
// <Badge count={null} /> -> null, not 5

go deeper

for a junior

Be able to write the modern form from memory — defaults go in the destructured parameter list — and say that a default applies when the prop is absent or undefined.

for a middle

Explain the exact trigger: undefined only, so 0, '' and null pass through untouched. Know that classes kept defaultProps and that React 19 dropped it for functions along with runtime propTypes.

for a senior

Show the second-order concern — object and function defaults create a fresh identity each render, which leaks into memoized children and dependency arrays — and how you would migrate a large component library safely.

for a principal

Own the policy question: whether runtime prop validation is still warranted anywhere, where untrusted input should actually be validated instead, and how a codemod-driven migration is sequenced across shared component packages.

## What changed Up to React 18, a component could carry a static `defaultProps` object and React would fill in any prop whose value was `undefined` before calling the component. React 18.3 warned that this was going away for function components, and React 19 removed it: the property is now ignored on function components. It is still supported for class components, which never gained an equivalent. ```javascript // React 18 style — no longer does anything in React 19 function Button({ size, variant }) { /* ... */ } Button.defaultProps = { size: 'medium', variant: 'primary' }; // React 19 style function Button({ size = 'medium', variant = 'primary' }) { /* ... */ } ``` The migration is mechanical, which is why it makes a good interview question: the interesting part is not the syntax but knowing when a default actually applies. ## When a default fires A JavaScript destructuring default applies when the value is `undefined` and only then. That includes the case where the caller omitted the prop entirely, because a missing property reads as `undefined`. It does **not** include `null`, `0`, the empty string, `false` or `NaN`. ```javascript function Badge({ count = 5 }) { return <span>{count}</span>; } <Badge /> // 5 — prop absent <Badge count={undefined} /> // 5 — explicitly undefined <Badge count={0} /> // 0 — a real value, default does not apply <Badge count={null} /> // null — default does not apply ``` This is the same rule the old `defaultProps` followed, so migrating does not change behaviour — but candidates routinely claim defaults fill in "missing or empty" props, and the `0` and `null` cases are where that belief produces bugs. If you genuinely want `null` to fall back, write it explicitly: `const shown = count ?? 5;`. ## Defaults on a prop you also spread Destructuring lets you take defaults for the props you care about and collect the rest: ```javascript function Button({ size = 'medium', type = 'button', ...rest }) { return <button type={type} className={`btn btn-${size}`} {...rest} />; } ``` Note what happens if `rest` also carries a `type`: it cannot, because `type` was destructured out. That is a small but real advantage of destructuring defaults over the old static object — the defaulted name is removed from the rest object, so a spread cannot accidentally reintroduce it. ## Defaults for object and function props A default that is an object, array or function literal creates a **new value on every render**: ```javascript function List({ items = [] }) { /* new array each render */ } ``` For rendering this is harmless. It becomes a problem the moment that value is passed to a child that compares props by identity, or used as a dependency, because it is never equal to the previous one. When that matters, hoist the default to a module-level constant and reference it: ```javascript const EMPTY = []; function List({ items = EMPTY }) { /* stable identity */ } ``` This is the same trap as any inline object prop, and it applies equally to callback defaults. ## Typing defaults In TypeScript the prop should be declared optional and the default supplies the value: ```typescript type ButtonProps = { size?: 'small' | 'medium' | 'large'; label: string }; function Button({ size = 'medium', label }: ButtonProps) { return <button className={`btn-${size}`}>{label}</button>; } ``` Inside the body `size` is narrowed to the non-optional union, so you do not need a guard. Declaring the prop required and then supplying a default is a contradiction the type checker will hold you to at the call site. ## The related React 19 removal `propTypes` went the same way: React 19 no longer performs runtime prop-type checking for function components, so a `Component.propTypes` object is dead weight. The replacement is static typing, which catches the same class of mistake at build time and across the whole call graph rather than only where a component happened to render in development. If a codebase is untyped and genuinely needs runtime validation of props coming from outside — a CMS payload, a plugin API — that validation belongs in the data layer with a schema validator, not in a component's `propTypes`. ## Answering it well Say the migration in one line, then show you know the `undefined`-only trigger, then mention the identity trap for object defaults. That progression is what separates "I read the release notes" from "I have done this migration".

  • A component has size = 'medium' as a destructuring default, and a caller passes size={null}. What renders?
    The component sees `null`, not `'medium'`. Destructuring defaults fire only on `undefined`, so an explicit `null` passes straight through and typically renders as a missing class or a broken lookup. If null should also fall back, write it in the body with `size ?? 'medium'`, or stop passing null from the caller.
  • Why can a default value like items = [] cause an unnecessary re-render downstream?
    Because the literal is evaluated on every render, producing a new array each time. A memoized child comparing props by identity sees a changed prop and re-renders, and the same value used in a dependency array re-runs the effect. Hoist the empty array to a module constant so the default has a stable identity.
  • Does defaultProps still work anywhere in React 19?
    Yes — on class components. React 19 removed the handling only for function components, where destructuring defaults cover the same ground with plain JavaScript. Class components never had that option, so their static defaultProps object is still applied before render.

saying these in an interview costs you the question

  • Believing a default fills in null or empty-string props
  • Saying defaultProps still works on function components in React 19
  • Marking a prop required in TypeScript and then defaulting it
  • Assuming propTypes still validates props at runtime in React 19
  • Using an inline object default on a prop passed to a memoized child

context