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.
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:
| Layer | Files | What lives here |
|---|---|---|
| Atoms | 7 | Container, Button, Logo, SectionHeading, … |
| Molecules | 41 | Cards, form units, FaqItem, TemplateGallery |
| Organisms | 33 | Full page sections — Header, Hero, Footer |
| Templates | 11 | Page-level layout that orders organisms |
| Total | 92 | 0 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) | |
|---|---|---|
| Directive | none (the default) | "use client" at the top of the file |
| Runs where | Build/server only | Server for the initial HTML, then the browser |
Can use useState/useEffect | No | Yes |
Can take an onClick handler | No | Yes |
Can await data directly | Yes | No |
| Ships JS to the browser | No | Yes |
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 component | Class component | |
|---|---|---|
| Declaration | function Name(props) | class Name extends React.Component |
| Props access | Parameter destructuring | this.props |
| State | useState | this.state + setState |
| Side effects | useEffect | componentDidMount/DidUpdate/WillUnmount |
| Shared logic | Custom hooks | HOCs, render props |
| Server Components | Supported | Not supported |
| Error boundaries | Not supported directly | The only way, without a framework |
| Used here | 92 | 0 |
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
| Symptom | Cause | Fix |
|---|---|---|
You're importing a component that needs useState at build time | A 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 props | An onClick declared in a Server Component | The element needing the handler belongs in a "use client" file |
| Component renders nothing, no error | The 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 child | Returning a raw object or a Promise from the component body | Render 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 render | A hook called inside a condition, loop, or after an early return | Move every hook call to the top level of the function, unconditionally |
Adding "use client" pulls in far more JS than expected | The directive is inherited by everything that file imports | Push it down to the smallest component that truly needs it |
| A custom hook works in one component, throws in another | It's being called from a Server Component | Hooks 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.
