Skip to content
React rc components 4 min read

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 for null. Passing label={null} yields null, not "Submit". If null is 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:

AspectDestructuring defaultsdefaultProps
Function componentsRecommendedDeprecated (warns)
Class componentsNot availableSupported
Where defaults liveIn the signatureIn a separate static object
Type inference (TS)Inferred automaticallyNeeds extra typing
Applies toundefined valuesundefined 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 a useEffect dependency 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 defaultProps for 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 never undefined inside the component.
  • Remember defaults apply only to undefined; use ?? when null should 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.
Last updated June 14, 2026
Was this helpful?