DEV Community

ackermannQ
ackermannQ

Posted on

React Doesn't Scale. Your Structure Does.

Scalability in React has nothing to do with components - and everything to do with how you separate orchestration, effects, and logic.

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
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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
}
Enter fullscreen mode Exit fullscreen mode

Used from component:

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

🧠 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 }
}
Enter fullscreen mode Exit fullscreen mode

The component does this:

const { profile, usage, tags } = useCustomerDashboard(id)
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

Used in component:

useBillingWarning(customer)
Enter fullscreen mode Exit fullscreen mode

🧠 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() {
    /* ... */
  }
}
Enter fullscreen mode Exit fullscreen mode

From React:

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

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

🧠 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.

Top comments (0)