React Doesn't Scale. Your Structure Does.

May 22, 2025

React feels scalable - until you realize your team is afraid to open certain files.

“We had a component called CustomerDashboard. 900+ lines. It fetched data, handled feature flags, did date formatting, triggered analytics, and contained three modals. Nobody knew what was safe to touch.”

This isn’t rare. And it’s not React’s fault. It’s ours - for not building structure around it.

React is a rendering library, not a system architecture. If you don’t explicitly define your system, your component tree will become it.


1. Where React stops helping

React is fantastic at rendering state. But beyond JSX and useEffect, you’re on your own.

You’ll start seeing this:

  • Hooks become orchestration engines
  • Logic spreads across multiple useEffects
  • Component props become a chain of derived state
  • Reusable logic becomes magic logic

🔥 Anti-pattern: Hook-as-everything

function useSubscriptionFlow(userId: string) {
  const [state, setState] = useState("idle")

  useEffect(() => {
    if (!userId) return

    track("SubscriptionStarted")
    fetchData(userId).then(res => {
      if (res.status === "error") router.push("/error")
      else setState("done")
    })
  }, [userId])

  return state
}

✅ What’s wrong:

  • It tracks analytics
  • Performs network fetch
  • Triggers navigation
  • Manages UI state

One hook. Four responsibilities. Zero clarity.


2. What scales: structure

The only way to make React codebases scale is to introduce separation of roles:

  • Rendering vs orchestration vs logic
  • Effects vs queries vs business flows
  • Triggers vs processors

Let’s look at concrete structures I now use.


3. Flow isolation: move business logic out of components

// flows/createCustomer.ts
export async function createCustomerAndLog(input: FormData) {
  const customer = await api.createCustomer(input)
  await analytics.track("CustomerCreated", { id: customer.id })
  return customer
}

Used from component:

const handleSubmit = async () => {
  await createCustomerAndLog(form)
  router.push("/success")
}

🧠 This lets components trigger, not orchestrate.


4. Pattern: Hook façade

Used in real-world design systems and dashboards:

function useCustomerDashboard(customerId: string) {
  const profile = useCustomerProfile(customerId)
  const usage = useCustomerUsage(customerId)
  const tags = useCustomerTags(customerId)

  return { profile, usage, tags }
}

The component does this:

const { profile, usage, tags } = useCustomerDashboard(id)

This isolates data-fetching composition, not UI logic. Think of it as the container pattern for hooks - but composable and invisible.


5. Pattern: Effect isolator

You’ll find this in apps like Vercel’s dashboard, Sentry’s audit logic, and internally at Stripe.

function useBillingWarning(customer: Customer) {
  useEffect(() => {
    if (customer.balance > 1000) {
      toast.warning("Your balance is overdue")
    }
  }, [customer])
}

Used in component:

useBillingWarning(customer)

🧠 This makes effects pluggable and separable.
No business logic in the UI. No hidden triggers. Each concern has a name.


6. Pattern: Imperative flow manager

Sometimes, declarative React doesn’t cut it. You want imperative orchestration - think onboarding flows, checkout steps, multi-API syncs.

Don’t shove this in hooks. Model it explicitly.

// services/accountMigration.ts
export class AccountMigrationManager {
  constructor(private readonly user: User) {}

  async start() {
    await this.exportData()
    await this.deleteLegacyAccount()
    await this.createNewAccount()
  }

  private async exportData() {
    /* ... */
  }
  private async deleteLegacyAccount() {
    /* ... */
  }
  private async createNewAccount() {
    /* ... */
  }
}

From React:

const manager = useMemo(() => new AccountMigrationManager(user), [user])

const handleMigrate = () => {
  manager.start().then(() => router.push("/done"))
}

🧠 Pattern seen in apps that require step-by-step orchestration - like Replay.io, or admin dashboards with workflow systems.


7. Final thoughts

React gives you rendering.
But only your structure can give you clarity, scale, and maintainability.

Build flows that narrate themselves.
Encapsulate effects with intent.
Write code that separates thought from display.

React doesn’t scale. Your structure does.


Profile picture
Quentin Ackermann

Full-stack engineer crafting developer tools in React, Node.js, and Unity - with a taste for AI, clean architecture, and creative problem-solving.

© 2025 brewed with