ForgeWP

State, Interactivity & Hydration

Use React state and events, understand automatic island discovery, and control hydration timing.

WordPress pages often combine mostly static content with a few interactive controls. ForgeWP lets you render the page content ahead of time and add browser JavaScript to the components that need it.

ForgeWP renders the app during export and supports selective client-side interactivity:

  1. The initial page markup is rendered before the theme is installed in WordPress.
  2. Components that need no browser behavior can remain rendered markup without an island entry.
  3. The compiler can discover and split eligible interactive components. Components that do not match supported patterns may need an explicit <Hydrate> boundary or another documented entry point.
Do I need to add `'use client'` at the top of interactive components?

No. ForgeWP does not use Next.js's 'use client' directive. Its compiler analyzes source for supported interactive patterns, including many React hooks and event handlers, and discovers eligible islands. Use <Hydrate> explicitly to control a component's hydration boundary or loading trigger, then verify its exported behavior.


Static HTML vs Interactive Islands

Before adding state to a component, consider its purpose: Does this component need to respond to user interactions in the browser after the page has loaded?

Static vs Island Architecture
[Layer 1] Rendered Markup (no island entry when no browser behavior is needed)
  - <SiteHeader />     Static header markup & desktop navigation
  - <HeroSection />    Hero copy, typography & banners
  - <ArticleBody />    Gutenberg content & typography
  - <SiteFooter />     Copyright, footer links & colophon

[Layer 2] Selective Hydration Islands (Client JavaScript)
  - <MobileNavDrawer />        Interactive toggle & drawer state
  - <LiveSearchModal />        Search query state & keyboard traps
  - <ProductGalleryCarousel /> Touch gesture handling & active slide
  - <NewsletterPopup />        Form submission state & localStorage check
Table · 4 columns
Component Type
Examples
Compiler Behavior
Client JS Overhead
Non-interactive component
Post cards, article bodies, footers
Rendered into the generated theme
No island bundle for this component
Interactive island
Mobile drawers, modals, filter bars
Initial markup plus client hydration when configured/discovered
Island and any shared dependencies

React State & Event Handling in Themes

ForgeWP themes use standard React 19 state and effect primitives. When you write interactive logic, code it exactly like standard React:

1. Managing Local State with useState

A common requirement in WordPress themes is an interactive accordion (e.g. for FAQs or collapsible sidebar sections):

src/components/AccordionItem.tsx
1import { useState } from "react"
2
3interface AccordionItemProps {
4  question: string
5  answer: string
6}
7
8export function AccordionItem({ question, answer }: AccordionItemProps) {
9  const [isOpen, setIsOpen] = useState(false)
10
11  return (
12    <div className="border-b border-zinc-200 py-4">
13      <button
14        onClick={() => setIsOpen(!isOpen)}
15        className="flex w-full items-center justify-between text-left font-semibold text-zinc-900"
16        aria-expanded={isOpen}
17      >
18        <span>{question}</span>
19        <span className="ml-4 text-xl text-zinc-400">{isOpen ? "−" : "+"}</span>
20      </button>
21
22      {isOpen && (
23        <div className="mt-3 text-sm leading-relaxed text-zinc-600">
24          {answer}
25        </div>
26      )}
27    </div>
28  )
29}

Because this component uses useState and an onClick event handler, it matches common interactive patterns that ForgeWP can discover. Confirm the resulting island in the export diagnostics and browser preview.

2. Browser Effects with useEffect

Use useEffect for operations that interact directly with browser APIs (window events, keyboard shortcuts, or local storage):

src/components/ScrollToTop.tsx
1import { useState, useEffect } from "react"
2
3export function ScrollToTop() {
4  const [visible, setVisible] = useState(false)
5
6  useEffect(() => {
7    const handleScroll = () => {
8      setVisible(window.scrollY > 400)
9    }
10
11    window.addEventListener("scroll", handleScroll, { passive: true })
12    return () => window.removeEventListener("scroll", handleScroll)
13  }, [])
14
15  if (!visible) return null
16
17  return (
18    <button
19      onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
20      className="fixed right-6 bottom-6 z-40 rounded-full bg-zinc-950 p-3 text-white shadow-lg transition hover:bg-zinc-800"
21      aria-label="Scroll to top of page"
22    >
23      ↑
24    </button>
25  )
26}

The ForgeWP Auto-Hydration Engine

How does ForgeWP decide whether a component should be static or interactive?

During the pnpm export build process, ForgeWP's compiler runs an AST (Abstract Syntax Tree) Interactivity Checker across all components in src/app, src/components, and src/blocks.

The compiler classifies a component as an interactive island if it detects any of the following:

  1. State & Lifecycle Hooks: Calls to useState, useEffect, useRef, useReducer, or custom hooks that wrap them.
  2. Inline Event Handlers: Any JSX prop matching /^on[A-Z]/ (such as onClick, onChange, onSubmit, onKeyDown).
  3. Browser Globals: Direct references to window, document, localStorage, sessionStorage, or navigator.
  4. Client Animation Libraries: Imports from animation packages including framer-motion, motion, gsap, lenis, swiper, lottie-react, or three.
  5. Animation Directives: Props like whileInView, whileHover, whileTap, animate, or scrollTrigger.
Auto-Hydration Compilation Flow
Component Source Code (e.g. src/components/Accordion.tsx)
  -> AST Interactivity Analyzer
       * Contains `useState` or `onClick`?
       * YES -> Extract to .forgewp-islands/ chunk + generate hydration wrapper
       * NO  -> Keep rendered markup without this island entry

Automatic discovery reduces manual island setup for supported patterns. Use the analyzer/export output to inspect discovered islands and choose an explicit <Hydrate> boundary for components that need one.


Fine-Tuning Performance with <Hydrate />

Discovered islands use a default loading strategy. An explicit <Hydrate> boundary lets you choose when a particular component's client code loads.

If you want fine-grained control over when an island loads and executes its JavaScript bundle, wrap it with the <Hydrate /> component from @forgewp/react:

src/components/SiteHeader.tsx (Using <Hydrate />)
1import { Hydrate } from "@forgewp/react"
2import { MobileMenu } from "./MobileMenu"
3import { SearchDialog } from "./SearchDialog"
4
5export function SiteHeader() {
6  return (
7    <header className="flex items-center justify-between border-b px-6 py-4">
8      <a href="/" className="text-xl font-bold">
9        StudioPress
10      </a>
11
12      {/* Plain links do not need client-side behavior by themselves. */}
13      <nav className="hidden gap-6 text-sm md:flex">
14        <a href="/blog">Blog</a>
15        <a href="/about">About</a>
16        <a href="/contact">Contact</a>
17      </nav>
18
19      <div className="flex items-center gap-3">
20        {/* Hydrates only when the user clicks the search icon */}
21        <Hydrate trigger="click">
22          <SearchDialog />
23        </Hydrate>
24
25        {/* Hydrates immediately on page load because mobile users need navigation instantly */}
26        <Hydrate trigger="load" media="(max-width: 768px)">
27          <MobileMenu />
28        </Hydrate>
29      </div>
30    </header>
31  )
32}

Hydration Triggers Reference

The trigger prop dictates the browser condition required to load and execute the island's JavaScript chunk:

Table · 3 columns
Trigger
When Hydration Occurs
Recommended Use Case
visible (Default)
When the component enters the viewport (via IntersectionObserver).
Carousels, review widgets, comment sections, infinite scroll.
load
Immediately on page load (DOMContentLoaded).
Critical above-the-fold UI: mobile hamburger drawers, notification bars.
interaction
When a click, mouseenter, or focusin event occurs on the element.
Interactive tab switchers, pricing calculators, currency pickers.
click
Only when the user explicitly clicks the element.
Search dialogs, confirmation modals, lightbox viewers.
hover
When the user hovers (mouseenter) over the element.
Mega-menus, hover cards, preview popovers.
idle
During browser idle periods using requestIdleCallback.
Background analytics widgets, live chat bubbles, feedback widgets.

Advanced Optimization Props

  • preload="near-visible": Pre-downloads the component's script when the user scrolls within ~600px of the viewport, so it mounts with zero delay upon entering view.
  • media="(max-width: 768px)": Restricts hydration to screens matching the CSS media query, saving bandwidth on devices where the feature is hidden.
  • clientOnly={true}: Bypasses server-side rendering entirely for components relying heavily on browser APIs (Canvas, 3D models, third-party iframe embedders).

Looking for WordPress Data Hooks?

All WordPress data hooks (useWpTitle, useWpContent, useWpQuery, useWpMenu, useWpCustomField, useWpOption, useWpThemeMod, etc.) are explored in exhaustive depth in the WordPress Data Layer. Many WordPress data hooks can be used in server-rendered markup without making that component interactive. useWpQuery() is different: it supports client-side query state and pagination, so use it within an interactive component. See Loops, Custom Post Types & Queries and Selective Hydration.


Next Up: Styling & Design Systems

Proceed to Styling & Design System to configure typography, fluid layout scales, the Font Orchestrator, and bidirectional Tailwind CSS theming.