Skip to main content
ASoc
Tutorial

React Functional Components: 92 of Them, Only 24 Reach the Browser

Zero class components here. The split that replaced function-vs-class is 68 Server Components shipping no JS against 24 client ones that need state.

The ASoc Team9 min read

A React functional component is a JavaScript function that takes a props object and returns JSX. This codebase has 92 of them and zero class components — no extends React.Component, no this.props, no this.state anywhere in src/. What's more interesting than the absence of classes is the split inside those 92: only 24 ever run in a browser.

The short answer

A function component is a function whose name is capitalised and whose return value is JSX:

// src/components/atoms/Container.tsx
export default function Container({
  className = "",
  children,
}: {
  className?: string;
  children: ReactNode;
}) {
  return <div className={`container ${className}`.trim()}>{children}</div>;
}

That's the entire contract. Props in, JSX out. State and lifecycle — the two things class components once held exclusively — are available through hooks (useState, useEffect), which is why the class form has been unnecessary since React 16.8 and is no longer taught in React's own documentation.

The census: 92 components, 0 classes

Broken out by this project's Atomic Design layers:

LayerFilesWhat lives here
Atoms7Container, Button, Logo, SectionHeading, …
Molecules41Cards, form units, FaqItem, TemplateGallery
Organisms33Full page sections — Header, Hero, Footer
Templates11Page-level layout that orders organisms
Total920 class components

Grepping for the class-era vocabulary — extends React.Component, extends Component, React.PureComponent, componentDidMount, this.props, this.state — returns nothing across src/. (One apparent hit is a code comment containing the English words "this state self-heals.")

For a project of this size that's unremarkable in 2026, and it's worth saying plainly rather than dressing it up: if you are starting a component today, write a function. The interesting question is no longer function versus class.

The split that replaced it: 24 client, 68 server

Here is the number that actually shapes how components get written now. Of the 92 component files, 24 carry the "use client" directive. The other 68 are React Server Components — they execute during the build, emit their output, and their code never reaches the browser bundle at all.

Both kinds are functional components. They look nearly identical on the page. They are not interchangeable:

Server Component (68 here)Client Component (24 here)
Directivenone (the default)"use client" at the top of the file
Runs whereBuild/server onlyServer for the initial HTML, then the browser
Can use useState/useEffectNoYes
Can take an onClick handlerNoYes
Can await data directlyYesNo
Ships JS to the browserNoYes

Container above is a Server Component — no directive, no hooks, nothing to hydrate. Button is too, which is why it can render an inert <span role="link" aria-disabled="true"> for a "Coming soon" CTA without a single line of client JavaScript.

FaqItem is the other kind, and it declares itself in the first line:

// src/components/molecules/FaqItem.tsx
"use client";
import { useId, useState } from "react";

export default function FaqItem({ question, answer }: FaqEntry) {
  const [open, setOpen] = useState(false);
  const id = useId();
  const buttonId = `${id}-button`;
  const panelId = `${id}-panel`;
  return (
    <div className="rounded-3xl bg-gray-50">
      <h3 className="m-0">
        <button
          type="button"
          id={buttonId}
          onClick={() => setOpen((o) => !o)}
          aria-expanded={open}
          aria-controls={panelId}
          className="flex w-full items-start justify-between gap-2 px-6 pt-6 pb-6 text-left"
        >
          {question}
          …

It owns open/closed state and an onClick. Either one alone forces "use client".

The practical rule this project follows: a component is a Server Component until it needs interaction state, a browser event handler, or a browser-only API — then, and only then, it gets the directive. The 68-to-24 ratio is what that rule produces on a content-heavy storefront, and it's the reason most of the site's markup costs nothing to hydrate.

Which hooks 92 function components actually use

Counted across src/components:

24  useState      12  useEffect      3  useWishlist    2  useRouter
 2  useRef         2  useMemo        2  useId          2  useActionState
 2  useOwnedProducts              1  useTransition    1  useCallback

Two things stand out. First, the list is short — 24 useState calls across 92 components means the overwhelming majority hold no state at all, which is what you'd expect when 68 of them can't. Second, useMemo and useCallback appear twice and once. The class-component era trained people to reach for memoisation reflexively; in practice a component that re-renders a handful of times per session does not need it, and the two useMemo calls here exist where a measurable list operation repeats, not on principle.

useWishlist and useOwnedProducts are this project's own custom hooks. That's the other thing function components made ordinary: a hook is just a function that calls other hooks, so shared stateful logic extracts into a plain function instead of the higher-order-component and render-prop wrappers classes required. The same flattening applies to composition — handing a component to another component as a prop happens 35 times here and needs no wrapper at all.

The one thing classes were still required for — and what replaced it

React's documentation is explicit that error boundaries have no hook equivalent: to catch a rendering error in a subtree, you need a class implementing componentDidCatch or getDerivedStateFromError. That is the last genuine capability gap between the two forms.

This codebase has no such class, because the framework supplies the boundary and hands you a function:

// src/app/error.tsx
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <main id="main" className="flex min-h-screen items-center justify-center px-4 text-center">
      <div>
        <h1 className="text-2xl font-bold text-title-color">Something went wrong</h1>
        <p className="mt-3 text-base text-text-color-secondary">
          An unexpected error occurred. Try again — if it keeps happening, email
          support@asoctemplates.com.
        </p>
        {error.digest && (
          <p className="mt-2 font-mono text-xs text-text-color-secondary">
            Reference: {error.digest}
          </p>
        )}

Next.js wraps the route segment in its own class boundary internally and renders this file when something throws, passing the caught error and a reset function as ordinary props. So the class still exists — it just isn't yours to write. If you are working in plain React with no framework boundary, this is the one place you will still type extends React.Component, or install a library that did it for you.

digest is worth noting since it explains the shape of this component: in production Next.js replaces the real error message with a hash before sending anything to the browser, and logs the same hash server-side. Rendering it is what lets a support email be matched to a server log line.

Function component versus class component

Function componentClass component
Declarationfunction Name(props)class Name extends React.Component
Props accessParameter destructuringthis.props
StateuseStatethis.state + setState
Side effectsuseEffectcomponentDidMount/DidUpdate/WillUnmount
Shared logicCustom hooksHOCs, render props
Server ComponentsSupportedNot supported
Error boundariesNot supported directlyThe only way, without a framework
Used here920

The "Server Components" row is the one that turned a style preference into a technical constraint. A class component cannot be a Server Component, so in an App Router project the class form doesn't merely feel dated — it opts every component written that way into the client bundle.

Troubleshooting

SymptomCauseFix
You're importing a component that needs useState at build timeA hook used in a file without "use client"Add the directive, or lift the state into a small client child and keep the parent on the server
Event handlers cannot be passed to Client Component propsAn onClick declared in a Server ComponentThe element needing the handler belongs in a "use client" file
Component renders nothing, no errorThe function returns nothing — often a { after the arrow instead of (=> (<div/>) returns; => {<div/>} does not. Add an explicit return
Objects are not valid as a React childReturning a raw object or a Promise from the component bodyRender a field of the object; in a Client Component, await in an effect or a Server Component instead
Rendered more hooks than during the previous renderA hook called inside a condition, loop, or after an early returnMove every hook call to the top level of the function, unconditionally
Adding "use client" pulls in far more JS than expectedThe directive is inherited by everything that file importsPush it down to the smallest component that truly needs it
A custom hook works in one component, throws in anotherIt's being called from a Server ComponentHooks only run in Client Components

Frequently asked questions

Are functional components slower than class components? No. They were marginally slower in very early React versions; that hasn't been true for years, and the trade now runs the other way — function components can be Server Components, which removes their JavaScript from the browser bundle entirely. 68 of the 92 here ship no client JS.

Do I still need to learn class components? Only to read existing code, and to write an error boundary in plain React. React's own documentation now teaches hooks first and treats the class API as legacy. Nothing in this codebase required a class.

Does every functional component need "use client"? No — it's the opposite default. In the App Router a component is a Server Component unless the file says otherwise. Add the directive only when the component needs state, an event handler, or a browser API. Here that's 24 files out of 92.

What makes a function a component rather than a plain function? Only how React uses it: a capitalised name and a JSX return value, invoked as <Thing /> rather than Thing(). A lowercase name renders as an HTML tag instead, which is the usual cause of a silently empty element.

Templates in this post

ASoc Watt, ASoc Willow and ASoc Anvil are Next.js + Tailwind ecommerce templates built on the component conventions audited above — function components throughout, "use client" pushed down to the units that genuinely need interaction state.

Browse the full sets: Next.js shop templates, Tailwind shop templates.

Keep reading

Tutorial9 min read

React Hooks: Why This Codebase's `useContext` Count Is Zero

24 useState, 13 useEffect, 0 useContext across 22 files — a real hook census, and the module-scope store pattern this codebase uses instead of Context.

Read more
Tutorial8 min read

Skipping React Hook Form: What Four Real Forms Look Like Without One

Login, signup, password reset and contact all run on FormData, a Server Action and useActionState — zero form libraries in package.json. Here's the actual validation code.

Read more
Tutorial10 min read

React Landing Pages: Half the HTML Isn't the Page

This storefront's home page prerenders to 436 KB, and 52.1% of that is a serialized copy of the render, not the page. What actually reaches a crawler before any JavaScript runs.

Read more