We often start with a button and end up maintaining a system. The hard part is not writing the first component. It is keeping the code understandable as requirements change.

These notes describe an incremental approach: understand what changes, establish boundaries, and only then introduce abstractions.

Follow the direction of change

Before splitting a component, ask three questions:

  1. Which pieces change together when requirements change?
  2. Which behaviors stay consistent across pages?
  3. How much internal knowledge does a caller need?

A product card and an article summary might both have a title, image, and description. Their business behaviors can still be very different. Similar appearances do not always justify a shared abstraction.

Keep together what changes together. Separate what changes for different reasons.

Make interfaces express intent

A clear component interface tells its caller what it can do. Instead of passing a dozen boolean flags, use a state that describes the situation directly.

components/save-button.tsx
type SaveState = 'idle' | 'saving' | 'saved'
 
function SaveButton({ state }: { state: SaveState }) {
  const labels = {
    idle: 'Save article',
    saving: 'Saving…',
    saved: 'Saved',
  }
 
  return (
    <button disabled={state !== 'idle'}>
      {labels[state]}
    </button>
  )
}

Mutually exclusive states reduce invalid combinations. A caller no longer needs to guess what happens when both loading and success are true.

Start with composition

When the layout is stable but the content varies, try children or named slots first. This preserves a useful boundary without adding every possible business case to the underlying component.

Separate three concerns

LayerResponsibilityExamples
PrimitivesInteraction and accessibilityButton, Dialog
Domain componentsBusiness meaning and presentationArticleCard, AuthorBio
PagesData and compositionHome, article detail

This is a communication tool, not a mandatory folder structure. Small projects can stay flat. Add structure when real complexity appears.

Leave room for the next edit

Before each commit, check whether names are clear, data lives near its consumers, and duplicated code has revealed a stable shared pattern.

Architecture does not have to predict every future. It only needs to make the next reasonable change possible without rebuilding everything. Let the code express today’s needs honestly, then find order through iteration.

Thanks for reading.

There’s always more to explore. See you in the next one.