Choosing a React Architecture: SPA, Frameworks, and the Future of Data Fetching

Thu Nov 14 2024

Data-fetching strategies in React have become a hot topic, and for good reason. They are tightly coupled to your app’s architecture, meaning the decision to build a Single Page Application (SPA) or use a framework like NextJS or Remix often dictates your approach to fetching data. In fact, the React team's own documentation suggests frameworks for most projects—and compelling reasons.

This post explores why the choice between SPA and a framework matters so much, the trade-offs between client-side and server-side fetching, and how modern tools like React Router, loaders, and React Server Components (RSCs) are changing the landscape of React development.


Should You Use a Framework?

React frameworks like NextJS and Remix are often seen as hybrids, blending the best aspects of SPAs and server-rendered apps (MPAs). They aim to avoid the pitfalls of SPAs, such as network waterfalls and lack of preloading, while retaining their interactivity and seamless navigation.

The React team is clear in their recommendation:

"If you’re building a new app or a site fully with React, we recommend using a framework."
React Docs

Without a framework, SPAs rely on client-side data-fetching strategies—often using hooks like useEffect—which the React team no longer recommends. Let’s unpack why that is.


What's Wrong with Client-Side Fetching?

SPAs popularized the approach of fetching data on the client, often triggered by React’s useEffect. However, there are several drawbacks to this method, as outlined by the React team:

1. Effects Don't Run on the Server

Fetching data with useEffect is inherently client-side. This prevents SPAs from leveraging server-side capabilities like pre-rendering or faster response times for initial data loads.

2. Network Waterfalls

Fetching data directly within components can lead to "waterfalls." For instance, a parent component fetches data, renders, and only then do child components fetch their data. This sequential fetching delays rendering and creates a stair-step pattern in your network tab:

GET /products/1     ▓▓▓▓▓▓▓
GET /brands          ▓▓▓▓▓▓
GET /products/related  ▓▓▓▓▓▓

Frameworks mitigate this by fetching all necessary data in parallel, even before rendering.

3. No Preloading or Caching

With useEffect, you can’t preload data before a component renders, nor can you efficiently cache responses without extra effort. By contrast, tools like Tanstack Query and React Router loaders provide robust caching and preloading capabilities.

4. Ergonomic Challenges

Using useEffect for data-fetching requires managing race conditions, cleanup logic, and edge cases manually. Frameworks and third-party tools simplify these tasks with dedicated APIs.


Alternatives to useEffect

If useEffect isn’t ideal, what should you use instead? The React team offers two main recommendations:

  1. Use a framework like NextJS or Remix.
  2. For SPAs, use tools like React Router loaders or Tanstack Query.

Using React Router Loaders

React Router’s loader functions fetch data outside of React components. This avoids waterfalls by parallelizing data-fetching for all matched routes.

Example: Fetching Data with Loaders

Here’s how you can set up a loader in React Router:

import { loader as productLoader, ProductPage } from '~/ProductPage'

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<RootLayout />}>
      <Route path="products" loader={productLoader} element={<ProductsSubLayout />}>
        <Route path=":productId" loader={productLoader} element={<ProductPage />} />
      </Route>
    </Route>
  )
);

export async function loader({ params }) {
  const product = await fetch(`/api/products/${params.productId}`).then((res) => res.json());
  return product;
}

export function ProductPage() {
  const product = useLoaderData(); // Access the loader's result
  return <div>{product.name}</div>;
}

This pattern avoids the pitfalls of useEffect, ensuring all necessary data is loaded before rendering.

Loaders + Tanstack Query

To combine React Router’s loaders with Tanstack Query’s caching power, you can use queryClient.ensureQueryData:

export async function loader({ params }) {
  return queryClient.ensureQueryData({
    queryKey: ['product', params.productId],
    queryFn: () => fetch(`/api/products/${params.productId}`).then((res) => res.json()),
    staleTime: 1000 * 30, // Cache for 30 seconds
  });
}

export function ProductPage() {
  const product = useLoaderData();
  return <div>{product.name}</div>;
}

This approach gives you the best of both worlds: robust caching and no waterfalls.


Fetching on the Server: Frameworks and React Server Components

Server-side fetching has gained traction thanks to frameworks like NextJS and Remix, which fetch data before sending HTML to the client. This reduces load times and eliminates the need for client-initiated fetching on the initial page load.

Framework-Specific Fetching

  • NextJS: Fetch data with getServerSideProps or getStaticProps.
  • Remix: Use loader functions, similar to React Router.

These approaches ensure data is ready when the page loads, avoiding the drawbacks of client-side fetching.

React Server Components (RSCs)

RSCs are a newer React feature designed to minimize client-side JavaScript by rendering components entirely on the server. Unlike traditional server-rendered components, RSCs don’t rehydrate on the client, meaning they can’t handle events or state.

Example: RSC + Client Component Boundary

async function ProductsServerComponent() {
  const products = await fetchProducts();
  return products.map((product) => (
    <ProductClientComponent key={product.id} product={product} />
  ));
}

function ProductClientComponent({ product }) {
  return <button>{product.name}</button>; // Interactive client-side component
}

RSCs are well-suited for static content or data-heavy components, while client components handle interactivity.


Which Architecture Should You Choose?

  • SPA: Suitable for simple, interactive apps. Use tools like React Router loaders or Tanstack Query to improve data-fetching ergonomics.
  • Framework: Best for modern apps with complex data-fetching needs. Frameworks provide out-of-the-box solutions for server-side fetching and rendering.
  • Hybrid: Frameworks like NextJS and Remix let you mix server and client components, giving you flexibility.

Conclusion

Your choice of React architecture—SPA or framework—dictates your data-fetching strategy. While frameworks are increasingly recommended for new projects, SPAs remain viable with tools like loaders and caching libraries.

Understanding the trade-offs between client-side and server-side fetching will help you make informed decisions as you build scalable, fast, and maintainable React apps.

Ready to dive deeper? Share your experiences or questions in the comments!

React Server Components (RSC)React RouterNextJSRemixSPA vs SSR ReactModern React architectureTanstack Query

Copyright © 2023 - All right reserved