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:
- The initial page markup is rendered before the theme is installed in WordPress.
- Components that need no browser behavior can remain rendered markup without an island entry.
- 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?
[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 checkComponent 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):
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):
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:
- State & Lifecycle Hooks: Calls to
useState,useEffect,useRef,useReducer, or custom hooks that wrap them. - Inline Event Handlers: Any JSX prop matching
/^on[A-Z]/(such asonClick,onChange,onSubmit,onKeyDown). - Browser Globals: Direct references to
window,document,localStorage,sessionStorage, ornavigator. - Client Animation Libraries: Imports from animation packages including
framer-motion,motion,gsap,lenis,swiper,lottie-react, orthree. - Animation Directives: Props like
whileInView,whileHover,whileTap,animate, orscrollTrigger.
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 entryAutomatic 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:
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:
Trigger When Hydration Occurs Recommended Use Case visible (Default)When the component enters the viewport (via IntersectionObserver).Carousels, review widgets, comment sections, infinite scroll. loadImmediately on page load ( DOMContentLoaded).Critical above-the-fold UI: mobile hamburger drawers, notification bars. interactionWhen a click, mouseenter, or focusin event occurs on the element.Interactive tab switchers, pricing calculators, currency pickers. clickOnly when the user explicitly clicks the element. Search dialogs, confirmation modals, lightbox viewers. hoverWhen the user hovers ( mouseenter) over the element.Mega-menus, hover cards, preview popovers. idleDuring 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.