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.