Frameworks

Building Reusable React Components for a Healthcare Portal: What I Learned

3 min read

For the last couple of years I've led frontend on a healthcare platform — patient and provider portals where the users include elderly patients on old Android phones, clinicians mid-shift, and screen-reader users, and where a broken form isn't a bounce, it's someone failing to reach their care. That context rewires how you build components. Here are the three lessons that survived contact with production, and one component that embodies them.

Why "good enough" isn't good enough here

On a typical SaaS product, a component that handles the happy path ships, and edge cases become bug tickets. In healthcare, the edge cases are the user base: slow networks in rural areas, motor-control constraints, screen readers, interrupted sessions. A component library that treats those as afterthoughts generates a steady tax of production incidents with real human cost. The economics flip — front-loading rigor into shared components is cheaper than any alternative.

Lesson 1: Design for the edge case first

The happy path is easy to add to a robust component; robustness is brutal to retrofit into a happy-path component. So every shared component in our library starts from a states inventory, not a mockup: loading, error, empty, disabled, partially-loaded, stale. The design question isn't "what does this look like?" but "what does this look like when things go wrong?"

The practical trick: make illegal states hard to represent. A data-display component takes one state prop shaped like { status: 'loading' } | { status: 'error', message } | { status: 'ready', data } rather than three independent booleans that can contradict each other. TypeScript's discriminated unions exist for exactly this.

Lesson 2: Keep components dumb — logic lives outside

Early versions of our patient-search component fetched its own data, managed its own cache, and knew about the API's pagination quirks. Reusing it in the provider portal — same UI, different endpoint and permissions — meant surgery. The rewrite made it presentation-only: it receives data and callbacks, renders states, and knows nothing about where data comes from. Hooks own the logic (usePatientSearch, useProviderSearch); the component just draws.

This isn't purity for its own sake. Dumb components are testable without mocking a network, reusable across contexts that fetch differently, and — the underrated one — reviewable: a PR touching data logic can't accidentally change rendering, and vice versa.

Lesson 3: Accessibility is a component-level requirement

Retrofitting accessibility screen-by-screen is how it never happens. Building it into the shared components is how it becomes free: if the Input component enforces a real <label>, announces its errors via aria-describedby, and manages focus correctly, then every form in both portals inherits that behavior without anyone remembering to add it. The component library is the accessibility policy, enforced by import.

The lessons in one component

A trimmed version of our form input — presentation-only, explicit states, accessible by construction:

type TextFieldProps = {
  id: string;
  label: string;
  value: string;
  onChange: (value: string) => void;
  error?: string;
  hint?: string;
  loading?: boolean;
  disabled?: boolean;
};
 
export function TextField({
  id, label, value, onChange, error, hint, loading, disabled,
}: TextFieldProps) {
  const hintId = hint ? `${id}-hint` : undefined;
  const errorId = error ? `${id}-error` : undefined;
 
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      {hint && <p id={hintId}>{hint}</p>}
      <input
        id={id}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        disabled={disabled || loading}
        aria-busy={loading || undefined}
        aria-invalid={!!error || undefined}
        aria-describedby={[hintId, errorId].filter(Boolean).join(' ') || undefined}
      />
      {error && (
        <p id={errorId} role="alert">
          {error}
        </p>
      )}
    </div>
  );
}

Nothing exotic — a real label, errors announced to assistive tech (role="alert", aria-describedby), loading and disabled handled as first-class states, zero knowledge of any API. Boring, and that's the point: reusable components succeed by being predictable, not clever. Build the rigor in once, and every screen that imports it gets healthcare-grade behavior by default — whatever industry you're actually in.

Found this useful?

Share

I post shorter takes on frameworks & architecture and frontend leadership on LinkedIn — follow along there, or get in touch if you're working on something related.