Abid Ali

Frontend Development

How to Build Reusable Components with TypeScript

By Abid Ali · Published · Updated

Type the props a component actually accepts, keep variants small, and move a component into a shared folder only when a second screen needs it.

Cover illustration: How to Build Reusable Components with TypeScript
Cover illustration.

Reusable react components are the ones a second screen can take without copying markup. TypeScript makes that contract visible: the props are the only inputs. If a caller has to know about internal state or a CSS class buried in the file, the component is not reusable yet. It is a snippet with a function around it.

Digital Dialogue's public components follow that split. PostCard renders one article preview. ShareButtons renders share actions. CalculatorField is a field used by more than one tool. None of those files also fetch the Contentful entry. The page passes data in. That is the pattern typed react components should keep.

Type the props you mean

Write the props as the data the markup needs, not as a bag of optional flags.

type FieldProps = {
  label: string;
  name: string;
  error?: string;
  children: ReactNode;
};

function Field({ label, name, error, children }: FieldProps) {
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      {children}
      {error ? <p id={`${name}-error`}>{error}</p> : null}
    </div>
  );
}

label and name are required because a field without them is a different component. error is optional because most renders have no error. The child is the input, so Field does not grow a prop for every input type.

Avoid props: any. Avoid a single data: object. The point of typescript component props is that a missing href fails at compile time, not in the browser.

Variants stay small

A button that is primary or quiet is one component with a variant. The markup is the same: an anchor or a button, a label, maybe an icon.

type ButtonProps = {
  href: string;
  variant?: "primary" | "quiet";
  children: ReactNode;
};

Map variant to a class. Do not accept arbitrary color props (bg, textColor, hoverBg) unless you are building a design-system primitive on purpose. Three colors passed from every call site means the design lives in the parent, and the "reusable" button is a div.

Component variants that change structure should stop. A card with an optional image is still a card. A card that sometimes becomes a quote, sometimes a form, and sometimes a chart is three components that happen to share a border.

Share it on the second use

Copying a card once is cheaper than a premature abstraction. The usual rule:

  1. Build it for the first screen, typed, with the real content as props.
  2. When the second screen needs the same markup, move the file to a shared folder.
  3. Delete any prop that only the first screen used, or give it a default.

Meme Tap's sound control is the case for step 2. Search results, trending, and favourites all need a large button, a label, and the same actions (favourite, download, share). One typed component takes the sound record. Three pages do not each invent a button.

type SoundButtonProps = {
  title: string;
  href: string;
  onPlay: () => void;
};

Favourites stored on the device stay outside the button. The button receives a boolean, saved, and a callback. It does not read local storage itself. That keeps the component usable in a list that already knows the saved set.

Props that leak

Watch for these:

  • A prop named isHome or isBlog that switches markup. Pass the difference as children or split the component.
  • Event handlers typed as Function. Type the event, or pass a () => void if the child does not need the event object.
  • Optional props that the component then treats as required (props.label.toLowerCase() when label might be undefined). Narrow them.
  • Spreading {...rest} onto a DOM node without an explicit type. It is convenient and it also lets a caller pass an invalid attribute with no error.

A component is done when a stranger can call it

Read the props and nothing else. Can you render it with a title and an href? If you also need to copy a hook, a context provider, and a global class, it is not a shared component yet. Move the hook to the parent, or document the provider as part of the contract and type the context value.

Reusable react components stay boring: a small prop type, markup that matches one role, and styles that belong to that role. The page decides what data to pass. TypeScript's job is to make the wrong pass a compile error.

Questions

Should every component accept a className prop?
Add className when a parent must adjust layout, such as margin or width. Do not add it so callers can restyle the component's internal colors and type. That splits one design into many.
Where should the prop type live?
Keep typescript component props in the same file as the component until another module needs the type. Export it when a parent builds the props object separately.
Is a variant prop better than two components?
Use a variant when the markup is the same and only the style or a small slot changes. Use two components when the structure is different. A button and a card are not variants of each other.

Next in the reading path: How to Fix Missing Meta Descriptions in Next.js

Let's discuss your next project.

Tell me what you are building, who it is for and what a good result looks like. I'll reply with questions, a suggested approach and the next steps.