Paulund
Subscribe

Default Props and Optional Props in React

1 May 2026, 4 min read

From the archive. Versions and APIs may have moved on since I wrote this.

On this page
  1. Typing Optional Props
  2. Setting Defaults with Destructuring
  3. Why I Stopped Using defaultProps
  4. Optional Props with Rest Patterns
  5. When Defaults Belong in the Parent Instead
  6. What Changed in How I Write Components

When I first started using React with TypeScript, I treated every prop as required. If a component needed a label, I made the label mandatory. If it needed a size, I passed a size. My components were honest, but they were also noisy. Every call site had to specify every value, even when the obvious default was staring me in the face.

Then I discovered optional props and default values. The problem was that React has two different ways to set defaults. One is the modern destructuring approach. The other is the defaultProps class component API, which React has deprecated for function components. I mixed them up for longer than I care to admit. This is what I wish I had known on day one.

Typing Optional Props

In TypeScript, you mark a prop as optional by adding a question mark to its key. The compiler then knows the caller can skip it, and the component has to deal with the missing value.

TSX
type ButtonProps = {
  label: string;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
};

function Button({ label, variant, disabled }: ButtonProps) {
  return (
    <button
      className={variant === 'secondary' ? 'btn-secondary' : 'btn-primary'}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

Here label is required. variant and disabled are optional. TypeScript will complain if you forget the label, but it will happily accept <Button label="Save" /> without the other two.

Inside the component, TypeScript sees variant as string | undefined and disabled as boolean | undefined. Try to use them without a check and the compiler complains. Defaults fix that.

Setting Defaults with Destructuring

The cleanest way to give an optional prop a default value is to assign it directly in the destructuring pattern.

TSX
function Button({
  label,
  variant = 'primary',
  disabled = false,
}: ButtonProps) {
  return (
    <button
      className={variant === 'secondary' ? 'btn-secondary' : 'btn-primary'}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

Now variant is always 'primary' | 'secondary' and disabled is always boolean. TypeScript knows the defaults fill the gaps, so the undefined possibility disappears after destructuring. The types are tighter. You read the signature and you know exactly what happens.

This works for objects and arrays too, but be careful. A default object literal in the destructuring pattern is recreated on every render. If you pass that object down to a memoized child, it will break the memoization.

TSX
type ChartProps = {
  data: number[];
  options?: { color: string; height: number };
};

// This creates a new object reference every render
function Chart({ data, options = { color: 'blue', height: 300 } }: ChartProps) {
  // ...
}

If Chart or its children rely on reference equality, pull the default out to module scope instead.

TSX
const defaultOptions = { color: 'blue', height: 300 };

function Chart({ data, options = defaultOptions }: ChartProps) {
  // ...
}

Why I Stopped Using defaultProps

Class components in React have a static defaultProps property. It used to be the idiomatic way to set defaults, and for a while the same API was supported on function components.

TSX
function Button({ label, variant, disabled }: ButtonProps) {
  return <button className={variant}>{label}</button>;
}

Button.defaultProps = {
  variant: 'primary',
  disabled: false,
};

React deprecated defaultProps for function components. It splits the component contract in two: you have to read the type definition, the function signature, and the static property just to understand what the component actually receives. Destructuring keeps everything in one place.

TypeScript also has a harder time inferring types from defaultProps. The static assignment is not checked as tightly as an inline default, and you lose autocomplete inside the component body because TypeScript sees the props as potentially undefined until it notices the defaultProps annotation. Destructuring defaults solve both problems.

Optional Props with Rest Patterns

Sometimes you want to forward most props to an underlying element and only override a few with defaults. The rest pattern makes this straightforward.

TSX
type InputProps = {
  label: string;
} & React.ComponentPropsWithoutRef<'input'>;

function Input({ label, type = 'text', ...rest }: InputProps) {
  return (
    <label>
      {label}
      <input type={type} {...rest} />
    </label>
  );
}

Here type is optional and defaults to 'text', but every other valid input attribute is accepted through the spread. ComponentPropsWithoutRef gives you the full HTML attribute type, so you do not have to redeclare onChange, placeholder, or className manually.

When Defaults Belong in the Parent Instead

Not every missing prop should have a component-level default. If a prop is conceptually required but the parent often passes the same value, a wrapper component (or a higher-order component) is sometimes cleaner than baking the default into the leaf component.

TSX
function PrimaryButton(props: Omit<ButtonProps, 'variant'>) {
  return <Button {...props} variant="primary" />;
}

This keeps Button honest about what it needs while removing repetition at the call sites. I use this pattern when a design system has a small set of recurring configurations that would otherwise clutter every JSX expression with identical prop values.

What Changed in How I Write Components

I used to think optional props were a form of laziness, a way to avoid specifying everything. Now I see them as part of the component contract. A required prop is a question the parent must answer. An optional prop with a sensible default is a decision the component can make on its own. The fewer questions I force on callers, the more reusable the component becomes. The key is making those defaults visible in the function signature, not hidden in a static property or a separate configuration file.

Next in React

7 May 2026Refs and when useRef beats useState