Background Mobile

How to Use React.js for Single Page Applications (SPAs)

reactjs/
September 17, 2026
How to Use React.js for Single Page Applications (SPAs)

React.js has become one of the most widely adopted libraries for building Single Page Applications. As of 2024, it powers roughly 40% of all JavaScript-heavy web applications, according to the State of JS survey. If you're already comfortable with component-based thinking and the virtual DOM, this post goes beyond the basics and into the architectural decisions that actually matter when you're building a production SPA.

What Makes React a Good Fit for SPAs?

A Single Page Application loads one HTML document and dynamically updates content as users interact with it. No full-page reloads. The browser handles routing, state, and rendering entirely on the client side.

React fits this model well because of how it handles UI updates. The reconciliation algorithm in React 18 (Fibre architecture) computes the minimum set of DOM mutations needed and batches them. This keeps rendering predictable and fast, even as component trees grow.

The trade-off is that you're shipping more JavaScript to the browser upfront. A poorly optimised SPA can take 3–6 seconds to become interactive on a mid-range Android device on a 4G connection. That's a real cost. If your users are on slow networks or low-end devices, an SPA might be the wrong call, and a server-rendered approach with Next.js or Remix would serve them better.

Where SPAs genuinely shine: dashboard-heavy applications, internal tools, and anything with complex, stateful workflows where users spend a long time on a single session. Think analytics platforms, admin portals, or multi-step form flows.

How Should You Structure a React SPA at Scale?

This is where most projects accumulate debt. Early architectural decisions compound quickly.

Folder Structure

Flat structures work for small apps. For anything above 20 components, a feature-based folder structure is more maintainable. Group files by domain, not by file type.

src/
  features/
    billing/
      BillingDashboard.tsx
      useBillingData.ts
      billing.api.ts
    auth/
      LoginForm.tsx
      useAuth.ts

This means a developer can delete a feature by deleting a folder. That's a useful property.

State Management

React's built-in useState and useContext are sufficient for localised state. For cross-cutting state, the options are:

Library Best For Bundle Size (minified+gzip)
Zustand Simple global state ~1 KB
Redux Toolkit Complex state with DevTools ~11 KB
Jotai Atomic, fine-grained updates ~3 KB
React Query / TanStack Query Server state, caching ~13 KB

A common mistake is using Redux for server state. React Query or TanStack Query handles caching, background refetching, and stale data far better than manually managing fetch state in Redux slices.

Routing

React Router v6 is the standard. Use createBrowserRouter with data loaders rather than the older component-based <Route> syntax. Loaders let you fetch data before a route renders, which eliminates the waterfall of render-then-fetch patterns.

Lazy-load routes using React.lazy and Suspense. Each route chunk should ideally be under 50 KB gzipped. Use Webpack Bundle Analyser or Vite's rollup-plugin-visualizer to verify.

What Are the Real Performance Bottlenecks in a React SPA?

The virtual DOM is fast, but it's not free. Careless re-renders are the most common source of slowness.

React.memo prevents re-renders when props haven't changed. useMemo and useCallback memoize computed values and functions. The risk is over-memoization. Wrapping everything in useMemo adds overhead without benefit if the computation is cheap.

Profile first. React DevTools Profiler shows exactly which components re-render and why. Fix the top offenders, not everything.

Code splitting is non-negotiable in production. Without it, a React app can ship a 1–2 MB JavaScript bundle. With route-level splitting and dynamic imports, you can reduce the initial load to under 200 KB in most cases.

A few other specifics worth watching:

  • Virtualise long lists using react-window or @tanstack/virtual. Rendering 1,000 DOM nodes at once will degrade scroll performance.
  • Avoid anonymous functions in JSX that get recreated on every render.
  • Use the key prop correctly in lists. Stable, unique keys prevent unnecessary unmounts and remounts.

/// Not sure where to start?

Get the architecture before you commit

Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.

Authentication and Route Protection

SPAs handle authentication on the client, which means the token is stored in the browser. The two common options are localStorage and httpOnly cookies.

localStorage is simpler to implement but is vulnerable to XSS. If an attacker injects a script into your app, they can read the token. httpOnly cookies are not accessible from JavaScript, which removes that vector, but they require CORS and cookie configuration on the server.

For most production applications, httpOnly cookies with short-lived JWTs and refresh token rotation is the right pattern. The access token lives in memory (a React context or Zustand store), and the refresh token is stored in an httpOnly cookie.

Protect routes at the router level, not inside individual components. A ProtectedRoute wrapper in your router config is cleaner and harder to accidentally bypass.

Testing a React SPA Without Wasting Time

Unit tests for pure logic. Component tests for behaviour, not implementation. End-to-end tests for critical user flows.

The mistake most teams make is writing too many snapshot tests. Snapshot tests break every time you change a class name and give false confidence. They test that the output didn't change, not that the component works correctly.

Use React Testing Library. It encourages tests that interact with the DOM the way a user would, querying by role and label rather than by class or ID. This makes tests less brittle.

For end-to-end testing, Playwright is currently the better option over Cypress. It runs tests in parallel, supports multiple browsers natively, and has a more reliable auto-wait mechanism. Run E2E tests against a staging environment, not against mocks.

A reasonable coverage target: 70–80% on business logic, 40–60% on UI components. Chasing 100% coverage on UI components usually isn't worth the maintenance cost.

Deployment and Build Considerations

Vite is the current default for new React projects. It's significantly faster than Create React App (which is no longer actively maintained) and produces smaller bundles in most configurations.

For hosting, serving a React SPA from a CDN is straightforward. Vercel, Netlify, and AWS CloudFront all handle it well. The one configuration detail that trips people up: the server needs to serve index.html for all routes, not just /. Otherwise, a user who navigates directly to /dashboard/reports gets a 404.

Set up cache headers deliberately. The index.html should have Cache-Control: no-cache. Hashed JS and CSS assets can have Cache-Control: max-age=31536000, immutable. This means users always get a fresh entry point but load cached assets for everything else.

If your SPA needs to be crawlable, React 18's streaming SSR through Next.js or a custom Express renderer solves that. A pure client-side SPA is invisible to crawlers that don't execute JavaScript, which still includes some search engines and most social media link previewers.

Conclusion

Building a React SPA that holds up in production is mostly about disciplined architecture early on: sensible folder structure, the right state management tool for each type of state, route-level code splitting, and secure auth handling. The React API itself is mature and stable. The problems that cause rewrites are almost always organisational, not technical.

If you're starting a new SPA, use Vite, React Router v6 with loaders, TanStack Query for server state, and Zustand for anything else. That stack has minimal magic and is easy to reason about.

If you're inheriting one that's slow, open the React DevTools Profiler and Webpack Bundle Analyser before writing a single line of code. The bottleneck is usually visible within 20 minutes.


FAQ

Do you need a framework like Next.js to build a React SPA? No. Next.js is primarily for server-side rendering and static generation. A plain React app with Vite and React Router is a fully capable SPA. Use Next.js when you need SSR, SSG, or edge rendering. For client-only apps, the added complexity of a framework isn't always justified.

How do you handle deep linking in a React SPA? Configure your server to return index.html for all routes. React Router then picks up the URL and renders the correct component. Without this configuration, any route beyond / will return a 404 when accessed directly or on page refresh.

Is a React SPA good for SEO? It depends on the application. Client-rendered content is indexed by Googlebot, but with a delay. Social media crawlers and some other search engines won't execute JavaScript at all. If SEO matters significantly for your product, pre-rendering or SSR via Next.js is a better approach.

When should you not use an SPA architecture? If your users are on unreliable networks, low-end devices, or if the application is primarily content-driven and needs fast first-paint, an SPA is a poor fit. Server-rendered HTML is faster to display and requires less client-side JavaScript. SPAs are best suited to application-like experiences with persistent session state.

How do you manage environment variables securely in a React SPA? Anything in a React bundle is public. Never put secrets, private API keys, or credentials in a .env file that Vite or CRA injects into the client bundle. Environment variables in a React SPA should only be public values like an API base URL or a Sentry DSN. Secrets belong on the server.

Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.

/// Work with us

Talk to the engineers who'd build it

You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.

Contact Us