Web development

Building React components that remain easy to change

A field guide to clear component boundaries, predictable props and accessible interactions.

By Nadir AMMI SAID 9 min read

Give each component one reason to change

I think about component boundaries through change. If the copy changes, the layout should not need a rewrite. If the interaction changes, a data model should not become a pile of event handlers. A component does not need to be tiny, but its responsibilities should be easy to name.

This is why I avoid putting filtering, API calls, keyboard behavior and visual markup into one reusable card. The first version may be fast, but every new requirement then touches the same crowded file.

  • Keep data shaping near the feature
  • Separate repeated UI from feature-specific decisions
  • Prefer explicit props over hidden global behavior

Make states visible in the types

A component usually has more states than its happy path: loading, empty, error, disabled and partially filled. TypeScript is useful when it makes those states visible before the browser does. A prop type should describe what the component can actually render, not just what the first call site happened to pass.

I prefer discriminated unions when states have different data requirements. That prevents a component from receiving an impossible combination such as `status: success` with no result, and it gives the render logic a clear shape.

  • Model loading and error states
  • Use narrow types for component contracts
  • Avoid optional props that hide impossible states

Keep interaction boundaries deliberate

Client-side state is not free. It adds code, hydration work and another place for behavior to drift from the server-rendered page. I keep state close to the interaction that owns it and avoid making a whole route interactive because one button needs a click handler.

Accessibility is part of that boundary. A custom visual control still needs keyboard behavior, a name and a meaningful disabled state. Native elements are often the most maintainable abstraction available.

  • Use native controls when they fit
  • Keep state local to the interaction
  • Test keyboard and focus behavior

Avoid abstractions that arrive too early

When two components look similar, I first check whether they change for the same reason. Sharing every line can make the code less readable if the components only happen to be similar today. A small amount of duplication is often cheaper than a generic component with a dozen flags.

I extract an abstraction when it removes a real decision or makes a repeated behavior safer. The test is practical: can a future change be made once without making unrelated screens harder to understand?

  • Extract repeated behavior, not just repeated markup
  • Keep variant names meaningful
  • Delete unused abstraction paths

Test behavior instead of implementation

The useful test is not that a component calls a particular hook. It is that a visitor can open the menu, submit the form, see an error and recover. Those tests survive refactors because they describe the contract at the boundary.

For visual pieces, a focused browser check and a few states often give more confidence than a large snapshot. The component remains easy to change because the test protects what matters, not its current DOM arrangement.

ReactTypeScriptTailwind CSS

Keep reading

Back to the journal