← Back to series

Frontend Development Fundamentals, Part 2: From Components to Applications

By Devsantara Team ·

A component is easy. An application is a few hundred components that all agree on how state, data, and composition work. Here's where that agreement tends to break down.

  • frontend
  • components
  • architecture

Part 1 covered the raw materials. This post is about what happens once you have more than a handful of components and need them to behave like one coherent application instead of a pile of independent widgets.

Composition over configuration

The first instinct with a reusable component is to give it props for every variation it might need. The alternative is composition: expose the pieces and let the caller assemble them.

// Configuration — every new variant means a new prop
<Card title="Post" showAvatar showTags variant="compact" />

// Composition — every new variant is just a different arrangement
<Card>
  <Card.Header>
    <Avatar />
    <Card.Title>Post</Card.Title>
  </Card.Header>
  <Card.Tags />
</Card>

Configuration wins for small, stable components. Composition wins once a component has been asked for its fifth boolean prop — that's usually the sign it's actually two or three components pretending to be one.

Where state actually lives

Most architecture problems aren't about components at all — they're about state living in the wrong place. A rough hierarchy that holds up across frameworks:

  1. Local state — UI state no other component needs (an input's focus state, a dropdown's open/closed flag)
  2. Lifted state — shared by a small, known set of siblings, held in their common parent
  3. URL state — anything that should survive a refresh or be shareable as a link (filters, pagination, the active tab)
  4. Server state — data that lives on a server and is cached locally (posts, users, anything fetched)
  5. Global client state — genuinely cross-cutting UI state (theme, auth session, toast queue)

The most common mistake is treating server state like global client state — hand-rolling a store for data a fetching library would cache, dedupe, and invalidate correctly on its own.

Data flow at the boundary

Where a component gets its data matters as much as how it renders. Server-rendered frameworks (Next.js, Remix, TanStack Start) push data-fetching to loaders that run before the component tree renders, rather than inside useEffect after it. That ordering removes an entire category of loading-waterfall and flash-of-empty-content bugs, at the cost of needing a clearer boundary between "server" and "client" code than a fully client-rendered app requires.

A rule of thumb for architecture reviews

When a codebase's component structure starts feeling unmanageable, the question worth asking isn't "do we need a bigger framework" — it's "does every piece of state here live at the lowest level that still satisfies every component that needs it?" Most sprawl traces back to state that got lifted one level higher than it needed to be, once, and never got questioned again.

Part 3 closes the series with what happens after the architecture is right: getting the application into production without it falling over on performance or accessibility.