ReactArchitecturePerformance

Building Scalable React Applications

March 15, 202410 min read

Scaling a React app is less about technical tricks and more about enforcing consistent patterns before complexity hits. The decisions you make at 5K lines of code will either serve you or haunt you at 50K.

Feature-First Folder Structure

Flat component folders collapse under real-world feature growth. Instead, co-locate everything a feature needs — components, hooks, types, API calls — in a dedicated directory.

src/
  features/
    auth/
      components/    # LoginForm, SignupForm
      hooks/         # useAuth, useSession
      api/           # auth.api.ts
      types.ts
    dashboard/
      ...
  shared/
    components/      # Button, Input, Modal
    hooks/           # useDebounce, useLocalStorage
    utils/

Tip: A good heuristic: if a component is used by more than one feature, move it to shared/. If it's only used within one feature, keep it co-located.

Custom Hooks for Logic

Components should be thin. Extract all non-rendering logic into custom hooks. This makes it easy to test, reuse, and replace implementation details without touching the UI.

// features/auth/hooks/useAuth.ts
export function useAuth() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)
 
  useEffect(() => {
    getSession().then(setUser).finally(() => setLoading(false))
  }, [])
 
  const logout = useCallback(async () => {
    await signOut()
    setUser(null)
  }, [])
 
  return { user, loading, logout }
}

State Management

Not every state needs a global store. Follow this decision hierarchy:

  • UI state (open/closed, selected tab) → useState in the component
  • Shared local state (form values across siblings) → lift to parent or useContext
  • Server state (API data) → React Query or SWR
  • Complex global state → Zustand or Jotai (avoid Redux for most apps)

Performance: Memoization Done Right

Over-memoization is a real problem. Wrapping everything in useMemo and useCallback adds complexity without benefit if the computation is trivial or the dependency array is unstable.

// ✅ Worthwhile — expensive computation, stable deps
const sortedItems = useMemo(
  () => items.slice().sort((a, b) => b.score - a.score),
  [items]
)
 
// ❌ Wasteful — object literal recreates every render anyway
const config = useMemo(() => ({ color: "red" }), [])
const config = { color: "red" } // simpler and equivalent

Note: Profile first with React DevTools Profiler before optimizing. Real bottlenecks are rarely where you expect them.

Absolute Imports

Relative import chains (../../../../components) are a refactoring nightmare. Configure path aliases so imports read like intent, not filesystem navigation:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
 
// Before
import { Button } from "../../../shared/components/Button"
 
// After
import { Button } from "@/shared/components/Button"

Summary

  • Use feature-first folder structure from day one
  • Keep components thin — move logic into custom hooks
  • Match state to the right tool (local, context, server, global)
  • Memoize only what profiling reveals as a bottleneck
  • Configure path aliases to keep imports readable