Skip to main content
ASoc
Comparison

React vs. Gatsby: The Real Question Is the GraphQL Layer

Gatsby adds a GraphQL data layer on top of React. This catalog imports typed data directly instead, with zero GraphQL and zero content plugins.

The ASoc Team8 min read

React is a UI library; Gatsby is a static site generator built on top of it that adds file-based routing, a GraphQL data layer, and a plugin ecosystem for pulling content in at build time. So "React vs. Gatsby" isn't really a choice between two things — you're writing React components either way. The actual decision is whether you want a GraphQL query layer sitting between your data and your components. This storefront answers that with zero: 139 React components import typed data directly, no query language in between.

What each layer actually adds on top of React

Plain React (this codebase)Gatsby
Static renderingBuilt into Next.js's App Router — pages prerender by default, generateStaticParams drives the dynamic onesGatsby's own build engine, prerendering every page to static HTML
Data accessimport { catalog } from "@/data/catalog" — a typed TypeScript array, read directlyEvery source (CMS, filesystem, APIs) gets normalized into one GraphQL schema, queried per-page with a graphql tag
Content authoring.mdx files compiled by @next/mdx, imported by an explicit slug → import() mapUsually gatsby-source-filesystem + gatsby-transformer-remark/MDX plugins feeding the GraphQL layer
RoutingApp Router file conventions ([slug]/page.tsx)Filesystem routes plus the createPages API in gatsby-node.js for programmatic ones
Mixing static and dynamicBoth live in one app: SSG catalog pages next to fully dynamic Server Actions and Route Handlers (checkout, downloads, dashboard), no separate deploy targetNeeds Gatsby Functions or Deferred Static Generation bolted on for anything that isn't build-time data
Extending to a new data sourceAdd a TypeScript file, import itInstall and configure a gatsby-source-* plugin, extend the GraphQL schema

Neither column changes what React itself does — components, props, hooks are identical in both. What changes is everything around React: how pages get their data, and whether that data has to pass through a query language to get there.

What Gatsby's data layer looks like, and what replaces it here

A Gatsby page typically pulls its content through a GraphQL query, even for data with no reason to be schema'd:

// The Gatsby way — this codebase does not do this
export const query = graphql`
  query {
    allProductsJson {
      nodes { slug name category }
    }
  }
`;
export default function Templates({ data }) {
  return data.allProductsJson.nodes.map((p) => <Card key={p.slug} {...p} />);
}

That query layer earns its keep when you're unifying genuinely disparate sources — a headless CMS, a spreadsheet, an Instagram feed, a dozen markdown collections — behind one interface every component can query the same way. This storefront has one source of product data, and it's already TypeScript:

// src/app/templates/page.tsx (real shape)
import { catalog } from "@/data/catalog";

export default function TemplatesPage() {
  return catalog.map((product) => (
    <TemplateCard key={product.slug} product={product} />
  ));
}

No schema translation, no query string to keep in sync with a type, no build step that has to run a GraphQL server in-process before it can render a single page. The type system (catalog: TemplateProduct[]) is the same guarantee GraphQL's schema gives you — that a product.slug really exists — enforced by tsc at next build instead of by a query resolver at data-fetch time. For 111 products and 476 static pages total, the entire "data layer" is TypeScript imports; there was never a second interface to build.

The same logic applies to content authoring. Gatsby typically transforms Markdown/MDX files into GraphQL nodes via gatsby-transformer-remark or gatsby-plugin-mdx, then queries them. This site's blog compiles .mdx directly with @next/mdx (remark-gfm for tables, rehype-slug for heading anchors — both named as plugin strings in next.config.ts, since Turbopack runs the pipeline in Rust and drops imported function references) and pairs each slug with its module through an explicit loader map in src/lib/blog.ts. A typo in that map is a build error, not a GraphQL query returning null.

One deploy target, not two execution models

The data layer isn't the only place Gatsby adds a seam. This storefront's build output mixes fully static product/catalog pages with fully dynamic paths — a checkout webhook, a gated download route, an authenticated dashboard, a per-request session refresh in src/proxy.ts (see What Is Supabase Used For for what that refresh actually does) — and none of it needed a second deployment target or a different runtime to add. The static pages prerender at build; the dynamic ones run as Server Actions and Route Handlers on the same Vercel deploy, same framework, same next build.

Gatsby's core model is build-time static generation, full stop — anything that needs to run per-request (an authenticated route, a webhook, a rate-limited download) is a second thing bolted on afterward: Gatsby Functions for serverless endpoints, Deferred Static Generation for pages too numerous or too fresh to prebuild all of, each with its own execution model and its own set of constraints layered on top of the static core. Neither addition is exotic, but they're additions — capabilities the static-first architecture didn't have and had to grow toward, rather than a spectrum the framework was built to span from day one.

That difference matters less for a pure content site — a blog, a marketing microsite with no login — where "everything is static" was already going to be true regardless of framework. It matters more the moment a project needs even a handful of dynamic routes alongside its static bulk, which is exactly this storefront's shape: hundreds of static catalog pages, a low double-digit number of dynamic ones, both shipping from one next build with no separate CI pipeline, hosting account, or plugin to configure for the dynamic half.

Common mistakes

MistakeWhat happensFix
Reaching for Gatsby because you need "static generation"Next.js, Remix, and even plain React with a static host all prerender — Gatsby isn't the only door to that outcomePick the meta-framework by its data model and routing, not by whether it can produce static HTML — most can
Treating "React vs. Gatsby" as a real either/orYou write React components in both; the difference is entirely in what wraps themFrame the decision as "do I want a GraphQL data layer," which is the actual variable
Querying local, already-typed data through GraphQL anywayAdds a schema, a resolver, and a build-time query for data you could have imported directlyImport the array; reserve GraphQL for genuinely federating multiple external sources
Bolting a dynamic route onto a Gatsby siteNeeds Gatsby Functions or DSG, a second execution model alongside the static buildIf a meaningful share of your app is dynamic (auth, checkout, a dashboard), a framework built around mixing both from the start avoids the seam
Assuming more plugins means more capability, with no costEach gatsby-source-*/gatsby-transformer-* plugin is a dependency to update and a potential build-time failure pointPrefer a data source you can import as code when one exists — fewer moving parts to break on upgrade

Frequently asked questions

Is "React vs. Gatsby" even a fair comparison? Not literally — Gatsby is built on React, so choosing Gatsby is still choosing React. The real comparison is React alone (or with a lighter meta-framework) versus React plus Gatsby's GraphQL data layer and plugin ecosystem.

When does Gatsby's GraphQL layer actually earn its keep? When you're genuinely unifying several disparate sources — a headless CMS, markdown files, and a couple of third-party APIs — behind one query interface that every component can use identically. If your data already lives in one typed place, as this catalog does, the query layer adds a translation step with nothing to translate.

Do I need Gatsby to build a blog with Markdown/MDX? No. This site's entire blog — sitemap, RSS feed, OG images, related-posts logic — runs on @next/mdx compiling .mdx files directly, paired with a typed metadata registry. No GraphQL server runs during the build.

Can I still use GraphQL in a plain React or Next.js app if I want to? Yes, with a client like Apollo or urql — it's an available tool, not something Gatsby owns. The point isn't that GraphQL is wrong; it's that it's an added layer you should choose for a reason, not inherit by default because a framework put it in front of you.

Does dropping Gatsby's data layer mean giving up build-time validation? No — it moves the check from a GraphQL schema to TypeScript's type system, which catches the same class of mistake at the same point in the pipeline. catalog: TemplateProduct[] fails next build the moment a product entry is missing a required field, the same way a query against a changed GraphQL schema would fail Gatsby's build — the validation didn't disappear, it runs as tsc instead of as a query resolver.

Templates in this post

ASoc Edge (an applied-AI agency site), ASoc Fade (a barbershop template) and ASoc Fiscal (a financial platform landing page) all ship on the same data model described above — typed catalog imports, zero GraphQL, zero content plugins.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For how this site's own MDX pipeline is wired, see MDX vs a Headless CMS; for the App Router routing conventions actually in use here, see Next.js Routing.

Keep reading

Comparison8 min read

React vs. Remix: A Mismatched Pairing, and the Real Question

Remix runs on React — they aren't competitors. The real comparison is Next.js vs. React Router v7, and it comes down to which way the static default points.

Read more
Comparison10 min read

React vs. Bootstrap: 74 KB of CSS for 420 Routes, Zero jQuery

React is a UI library, Bootstrap a CSS framework — the real fork is Tailwind vs. Bootstrap, measured against this site's single 74 KB stylesheet and zero jQuery dependency.

Read more