Skip to content

React Tutorial Advanced React Concepts

Designing Reusable React Components: Props API Patterns

Lesson 117 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson ยท press Try it on the examples to edit and run them here.

Reusable Components showed how to extract repeated markup. This lesson is about designing components so that they are pleasant to reuse across a whole product: the shape of their props, their flexibility and their defaults.

Design the props like an API

A component’s props are its public API. Good ones are:

  • Few. Every prop is something to learn, document and support.
  • Named for meaning, not appearance: variant="danger" rather than red.
  • Hard to misuse. Prefer a set of named options to free-form booleans.
  • Predictable. Props behave like their native HTML equivalents where possible.
App.jsx
JSX
function Button({ variant = "primary", size = "md", type = "button", children, ...rest }) {
  const styles = {
    primary: { background: "#1a5276", color: "#fff", border: "1px solid #1a5276" },
    secondary: { background: "#fff", color: "#1a5276", border: "1px solid #1a5276" },
    danger: { background: "#b71c1c", color: "#fff", border: "1px solid #b71c1c" },
  }[variant];
  const padding = { sm: "4px 10px", md: "8px 16px", lg: "12px 22px" }[size];

  // ...rest forwards onClick, disabled, aria-label and so on to the real <button>
  return (
    <button type={type} style={{ ...styles, padding, borderRadius: 6, cursor: "pointer" }} {...rest}>
      {children}
    </button>
  );
}

function App() {
  return (
    <div style={{ display: "flex", gap: 8 }}>
      <Button variant="danger">Delete account</Button>
      <Button variant="secondary" size="sm">Cancel</Button>
      <Button disabled>Disabled</Button>
    </div>
  );
}

Design notes:

  • variant and size are enumerations, not a set of booleans (isDanger, isSmall, isLarge) that can contradict each other.
  • Sensible defaults make the common case short: <Button>Save</Button>.
  • type="button" by default avoids the surprise of accidental form submission.
  • ...rest forwards native attributes, so disabled, onClick and aria-* all work.

Controlled and uncontrolled: support both

Native inputs can be controlled (value + onChange) or uncontrolled (defaultValue). A flexible component copies that:

App.jsx
JSX
function Toggle({ checked, defaultChecked = false, onChange, label }) {
  const isControlled = checked !== undefined;
  const [inner, setInner] = React.useState(defaultChecked);
  const value = isControlled ? checked : inner;

  function handleChange(e) {
    if (!isControlled) setInner(e.target.checked);
    onChange?.(e.target.checked);
  }

  return (
    <label>
      <input type="checkbox" checked={value} onChange={handleChange} /> {label}
    </label>
  );
}

function App() {
  const [on, setOn] = React.useState(false);
  return (
    <div>
      {/* controlled: the parent owns the state */}
      <Toggle label="Notifications" checked={on} onChange={setOn} />
      <p>Parent sees: {on ? "on" : "off"}</p>
      {/* uncontrolled: manages itself */}
      <Toggle label="Remember me" defaultChecked />
    </div>
  );
}

Uncontrolled use is the easy default; controlled use lets the parent coordinate. Warn if a component switches between the two modes during its life.

Polymorphism: the as prop

Sometimes the same look must be a link in one place and a button in another. Let the caller choose the tag:

App.jsx
JSX
function Action({ as: Tag = "button", children, ...rest }) {
  return (
    <Tag style={{ display: "inline-block", padding: "6px 14px", border: "1px solid #1a5276", borderRadius: 6, color: "#1a5276", textDecoration: "none" }} {...rest}>
      {children}
    </Tag>
  );
}

function App() {
  return (
    <div style={{ display: "flex", gap: 8 }}>
      <Action as="a" href="https://example.com/docs">Docs</Action>
      <Action onClick={() => {}}>Run</Action>
    </div>
  );
}

Choose the element for the semantics: links for navigation, buttons for actions.

Render props: let the parent decide the output

When a component knows how to do something but not what to show, accept a function that returns JSX:

App.jsx
JSX
function List({ items, renderItem, empty = <p>Nothing here</p> }) {
  if (items.length === 0) return empty;
  return <ul>{items.map((item, i) => <li key={item.id ?? i}>{renderItem(item)}</li>)}</ul>;
}

function App() {
  const fruits = [{ id: 1, name: "Apple", emoji: "๐ŸŽ" }, { id: 2, name: "Banana", emoji: "๐ŸŒ" }];
  return (
    <div>
      <h3>Items</h3>
      <List items={fruits} renderItem={(f) => <span>{f.emoji} {f.name}</span>} />
    </div>
  );
}

List handles the empty state and the keys; the caller decides how each row looks. Before hooks, render props were the main way to share logic. Today custom hooks usually do that better; use render props for customising output.

Slots and composition

For several areas the caller can fill, use named props that accept JSX (header, footer, icon), as in Component Composition.

Accessibility is part of the API

A reusable component should be accessible by default: real <button> and <a> elements, labels, focus styles, keyboard support and the right ARIA. Fix it once and every use benefits. Document which props are required for accessibility (for example, icon-only buttons need aria-label).

When not to generalise

  • Do not add props for hypothetical cases. Wait for a real second use.
  • Do not build a “mega component” with 20 props. Split it into smaller ones or compose.
  • Do not copy and tweak either. If two components differ by one thing, add one prop.

A helpful test: can a new teammate use this component correctly without reading its code?

Key points

  • Design props as a small, meaningful API with defaults and forwarded native attributes.
  • Support both controlled and uncontrolled use where it makes sense.
  • Use the as prop and render props sparingly, and build in accessibility.