Skip to main content
ASoc
Tutorial

React use client: Only 26 of 92 Components Need It Here

This storefront ships 92 components; only 26 carry "use client". What actually earns the directive, and the leaf-component pattern that keeps the rest server-only.

The ASoc Team8 min read

"use client" marks a module boundary: everything the file imports and renders ships to the browser and re-executes there. You need it only where a component holds state, attaches an event handler, or touches a browser API — never by default, and never to "make something interactive show up." In a 92-component storefront, exactly 26 files carry the directive.

The signal, not the default

The directive is easy to over-apply, because the failure mode of skipping it (a build error: "You're importing a component that needs useState…") is loud and the failure mode of over-applying it (a bigger client bundle, silently) is not. A component needs "use client" when it does at least one of these — and only then:

Needs "use client"Stays a Server Component
useState, useReducer, or any Hook that holds stateRenders from props/data only
useEffect / useLayoutEffectNo lifecycle, no browser-only work
An event handler (onClick, onChange, …)Static markup, or a handler passed down as a prop
Browser-only APIs (window, localStorage, IntersectionObserver)Server-only APIs (fs, a DB client, process.env secrets)
React Context via useContextReads props a Server Component parent already resolved

Everything else defaults to a Server Component, and that default is the one worth protecting — every file that doesn't need the directive and doesn't get it is markup and data-fetching that never reaches the client bundle at all.

What this repo actually marks client-side

grep -rl '"use client"' src/components src/app returns 26 files. The full list is small enough to read as a decision record for what actually needed it:

src/components/molecules/AuthCard.tsx        — password form + inline errors
src/components/molecules/BlogCtaLink.tsx     — click handler for one analytics event
src/components/molecules/BuyButton.tsx       — session check, checkout URL, LS overlay
src/components/molecules/ContactForm.tsx     — form state
src/components/molecules/DownloadMenu.tsx    — edition picker, open/close
src/components/molecules/EditionPicker.tsx   — selected-edition state
src/components/molecules/FaqItem.tsx         — accordion open/close
src/components/molecules/NewsletterForm.tsx  — form state
src/components/molecules/PreviewModal.tsx    — modal open state, device toggle
src/components/molecules/ProductDownloadGroup.tsx
src/components/molecules/PurchaseCta.tsx
src/components/molecules/RedemptionPicker.tsx
src/components/molecules/RefundButton.tsx
src/components/molecules/SavedTemplates.tsx
src/components/molecules/SettingsForm.tsx
src/components/molecules/TemplateCard.tsx    — hover state, wishlist, ownership lookup
src/components/molecules/TemplateGallery.tsx — carousel index, arrow-key nav
src/components/molecules/UseCaseCard.tsx
src/components/molecules/WishlistButton.tsx
src/components/organisms/DashboardTabs.tsx
src/components/organisms/Header.tsx          — mobile menu
src/components/organisms/SavedTemplatesGrid.tsx
src/components/organisms/TemplatesExplorer.tsx
src/components/organisms/YourProductsGrid.tsx

Every one of the 12 home-page organisms except Header — Hero, Trust, TechStack, Features, Carousel, UseCases, Plugins, Testimonials, Blog, Footer — stays a Server Component, because a marketing section that maps over a data array and renders links needs none of the five signals above. Header earns the directive for exactly one reason: the mobile hamburger menu holds open/closed state.

The accordion is the clean case

src/components/molecules/FaqItem.tsx is the smallest honest example — one boolean, one click handler:

"use client";
import { useId, useState } from "react";

export default function FaqItem({ question, answer }: FaqEntry) {
  const [open, setOpen] = useState(false);
  const id = useId();
  return (
    <div className="rounded-3xl bg-gray-50">
      <h3 className="m-0">
        <button
          type="button"
          onClick={() => setOpen((o) => !o)}
          aria-expanded={open}
          aria-controls={`${id}-panel`}
        >
          {question}
        </button>
      </h3>
      <div id={`${id}-panel`} /* … */>{answer}</div>
    </div>
  );
}

useState is the whole reason it's here. Nothing about the ARIA attributes (aria-expanded, aria-controls) requires the client — those are just props — but the toggle they describe does.

The pattern that actually saves bundle size: a leaf, not a section

The more interesting case is BlogCtaLink.tsx, because it exists specifically to keep a bigger thing server-rendered. Every article ends with a "Templates in this post" block built by RelatedTemplates, which resolves catalog data and renders several links. Only one thing about that block is interactive: firing an analytics event on click. Rather than marking the whole organism (and the article page around it) client-side, the click handler is carved into its own three-prop leaf:

"use client";
import Link from "next/link";
import { track } from "@/lib/analytics";

export default function BlogCtaLink({
  href,
  postSlug,
  targetSlug,
  className,
  children,
}: {
  href: string;
  postSlug: string;
  targetSlug: string;
  className?: string;
  children: React.ReactNode;
}) {
  return (
    <Link
      href={href}
      className={className}
      onClick={() => track("blog_cta_clicked", { post: postSlug, target: targetSlug })}
    >
      {children}
    </Link>
  );
}

RelatedTemplates still runs on the server, still resolves the catalog, still renders every link's markup server-side — it just delegates the one line that needs a browser (onClick) to a four-prop child. The client bundle for that block is one event handler and a track() call, not the catalog-resolution logic or the markup around it. TemplateCard.tsx is the opposite trade, made on purpose: it's client-side wholesale, because hover state, the wishlist button, and useOwnedProducts()'s ownership lookup are load-bearing for what the card does on every render, not one click at its edge — splitting it into a client leaf would just move the "use client" boundary one file up for no bundle savings.

The mental model this inverts

In a Create React App or Vite SPA, every component is a client component — there is no other kind, because the entire app is one JavaScript bundle that boots in the browser and renders from scratch. "use client" only exists as a concept in a framework that renders on the server by default, which means the question it answers is backwards from what most React tutorials teach: not "how do I make this run in the browser" (everything already does), but "which pieces actually need to."

That inversion is why the directive reads as unfamiliar even to developers who know React well — the default they learned was Client Component, full stop, and Server Components (and the boundary that opts back into the old default) postdate that default by years. PreviewModal and DownloadMenu are worth naming here because they show the boundary is per-interaction, not per-page: the product detail page (TemplateDetail) is a Server Component that fetches the catalog entry and renders the gallery, the description, the pricing — and only the modal that opens on top of it, and the edition picker inside that modal, cross into client territory. The page doesn't become a client component because one thing on it opens a dialog.

Mistakes and how they show up

SymptomCauseFix
"You're importing a Server Component that only works in a Server Component" or similar build errorA Server Component (or its data import) is rendered from inside a "use client" filePass the server-rendered thing down as children/props instead of importing it from the client file
useState build error with no "use client"The Hook was added to a file that was Server by defaultAdd the directive at the very top of the file, before any import
A section that renders static content still ships client JS"use client" was added to the whole organism instead of the one interactive leafCarve the handler into its own small component, the way BlogCtaLink isolates one onClick from RelatedTemplates
Client bundle grows after adding one small featureA new client component imports a large server-only-shaped module (data files, DB clients) transitivelyKeep data resolution in the Server Component parent; pass only the resolved primitives down as props
"use client" silently has no effectIt's not the first line of the file (a comment or import precedes it)It must be the literal first statement — comments above it are fine, code or imports are not

Frequently asked questions

Does "use client" mean the component only renders in the browser? No — a Client Component still renders once on the server for the initial HTML (unless it's inside <Suspense> with no fallback reached), then hydrates and can re-render in the browser after. The directive marks where the component's code is allowed to run again, not where it renders the first time.

Do I need "use client" on every file a client component imports? No, and adding it everywhere is the over-application failure mode this post opens with. The directive marks a boundary; everything a marked file imports is pulled across that boundary automatically. Put it on the leaf that actually needs interactivity, not on every ancestor.

Why does Header need it but none of the other home-page organisms do? Because the mobile menu's open/closed state is the only piece of client behavior anywhere in that page's organisms. Hero, Features, Testimonials and the rest map over a data array and render <Container>/<SectionHeading>/links — none of that needs a Hook, a handler, or a browser API.

Is there a cost to marking something client-side that doesn't strictly need it? Yes: every file the directive pulls in — its imports, the components it renders — ships to the browser as JavaScript, whether or not the interactivity you added actually needed all of it. That's the entire argument for a leaf like BlogCtaLink over marking RelatedTemplates client-side: one onClick versus an organism plus the catalog-resolution code behind it.

Templates in this post

ASoc Amplify is a social-media-management SaaS site with services, results stats, and a journal. ASoc Atelier is a designer's portfolio and studio site with a booking funnel. ASoc Axiom is a neural-networks AI-consultancy landing page with an FAQ accordion of its own. Every one of them ships at least one FaqItem-shaped interactive piece behind exactly this kind of boundary.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Tutorial9 min read

Robots.txt and Sitemap.xml in Next.js: One Declared Date, 420 Routes

The STOREFRONT_COPY_REVISED fix for a sitemap that told Google 111 pages changed on a date nothing did, plus the noindex-vs-disallow split this codebase actually uses.

Read more