Phase 12: React Hook Form Adapters Split

September 16, 2025 · View on GitHub

Objective

Create a separate, optional package @nexcraft/forge-rhf that ships React Hook Form (RHF) adapters for the Forge React bindings, eliminating runtime detection and ensuring clean ESM interop with Next.js + Turbopack.

Scope

  • Keep @nexcraft/forge focused on web components + React wrappers only.
  • Move RHF adapters into a new package @nexcraft/forge-rhf.
  • Use static imports (no require/eval/global sniffing).
  • Make react-hook-form a peer dependency of the new package.
  • Ensure Turbopack compatibility via ESM + transpilePackages guidance.

Deliverables

  • New workspace package: packages/forge-rhf (ESM only).
  • RHF adapters: Input, Select, Checkbox, RadioGroup (+ minimal types and a factory util if desired).
  • Cleaned base package: remove RHF code/exports from src/integrations/react.
  • Docs page: docs/integrations/react-hook-form.md with install and Next.js notes.

Milestones (1–2 days)

  • Day 1: Scaffold package, migrate adapters, adjust exports, build/type-check.
  • Day 2: Add docs, validate with Next.js example (Turbopack), polish.

Progress Checklist

  • Enable workspaces at root (private, workspaces) Alternative approach adopted
  • Scaffold packages/forge-rhf (pkg.json, tsconfig, README, src/*) Alternative approach adopted
  • Implement RHF adapters (Input, Select, Checkbox, RadioGroup) - Completed via subpath exports approach
  • Remove RHF code/exports from base @nexcraft/forge Alternative approach: kept RHF as optional subpath
  • Build base package successfully
  • Build @nexcraft/forge-rhf successfully N/A - using subpath approach
  • Type-check both packages (strict, no errors)
  • Validate in Next.js (Turbopack) with transpilePackages
  • Add docs: docs/integrations/react-hook-form.md - Comprehensive guide with integration patterns, Next.js compatibility, TypeScript support, and examples
  • Prepare publish (peer deps, files/exports, publishConfig) - Completed with subpath exports
  • Publish @nexcraft/forge-rhf (or dry-run) Published as part of main package
  • Announce and update examples - Examples updated and working

COMPLETED STATUS UPDATE

Alternative Implementation Adopted: Instead of creating a separate @nexcraft/forge-rhf package, we implemented a subpath exports approach that achieves all the same goals with better DX:

🎯 What Was Accomplished

  1. Static Imports Only: Eliminated all runtime detection (require/eval/global sniffing)
  2. Separate RHF Subpath: @nexcraft/forge/integrations/rhf - users opt-in explicitly
  3. Peer Dependencies: react-hook-form configured as optional peer dependency
  4. Next.js + Turbopack Compatible: Full ESM compatibility, no module resolution issues
  5. Working RHF Adapters: All adapters functional with proper synthetic event handling

🔧 Technical Implementation

  • RHF Adapters: src/integrations/react/rhf/ReactHookFormAdapters.tsx

    • RHFForgeInput: Direct event passing onChange: (event) => field.onChange(event)
    • RHFForgeSelect: Synthetic events { target: { value, name } }
    • RHFForgeCheckbox: Synthetic events { target: { checked, name, type: 'checkbox' } }
    • RHFForgeRadioGroup: Synthetic events { target: { value, name, type: 'radio' } }
  • Package Exports: Added subpath in package.json

    "./integrations/rhf": {
      "types": "./dist/integrations/react/rhf/index.d.ts", 
      "import": "./dist/integrations/react/rhf/index.js"
    }
    
  • Fallback Renderer Fixes: Updated ForgeInput/ForgeCheckbox to handle {...register()} spread properly

🧪 Validation & Testing

  • TypeScript Build: No compilation errors
  • Next.js Integration: Working at http://localhost:3000/forms-demo
  • Both Integration Patterns:
    • Direct {...register()} spread syntax ✅
    • RHF adapter components (RHFForgeInput, RHFForgeCheckbox) ✅
  • Form Interaction: All form elements fully interactive
  • Event Handling: Proper synthetic events for React Hook Form compatibility

📦 Usage Pattern

// Core Forge components
import { ForgeInput, ForgeButton } from '@nexcraft/forge/integrations/react';

// RHF adapters (separate import - users opt-in)  
import { RHFForgeInput, RHFForgeCheckbox } from '@nexcraft/forge/integrations/rhf';

// Works with both patterns:
<ForgeInput {...register('name')} />  // Direct spread
<RHFForgeInput name="message" control={control} />  // Adapter

🏆 Benefits of Subpath Approach

  1. Simpler DX: Single package installation vs separate package
  2. Better Versioning: No version drift between packages
  3. Cleaner Imports: Clear separation without package proliferation
  4. Same Goals Achieved: Static imports, optional RHF, Turbopack compatibility

Work Plan (Step‑By‑Step)

  1. Enable Workspaces (root)
  • Update root package.json:
    • Add "private": true and "workspaces": [".", "packages/*"].
    • Keep all existing fields and scripts intact.
  1. Scaffold @nexcraft/forge-rhf
  • Create packages/forge-rhf/ with:
    • package.json
    • tsconfig.json
    • README.md
    • src/index.ts, rhf-forge-input.tsx, rhf-forge-select.tsx, rhf-forge-checkbox.tsx, rhf-forge-radio-group.tsx
  • Package config (summary):
    • type: module, sideEffects: false
    • main/module: ./dist/index.js, types: ./dist/index.d.ts
    • exports: { ".": { types, import } }
    • peerDependencies: @nexcraft/forge, react, react-dom, react-hook-form
  1. Implement Adapters (static imports)
  • Pattern for each adapter:
    • import { Controller } from 'react-hook-form'
    • Import Forge React components from @nexcraft/forge/integrations/react.
    • Map RHF field → Forge props:
      • Input: field.valuevalue, onChange(value: string)field.onChange(value)
      • Select: value passthrough, supports string | string[]
      • Checkbox: field.valuechecked, onChange(checked: boolean)field.onChange(checked)
      • RadioGroup: field.valuevalue, onChange(value: string)field.onChange(value)
  1. Clean Base Package (remove RHF)
  • In @nexcraft/forge:
    • Remove RHF exports from src/integrations/react/index.ts.
    • Delete src/integrations/react/adapters/ReactHookFormAdapters.tsx.
    • Keep only React wrappers and utilities.
  1. Build & Type-Check
  • Install once at root: npm i
  • Build base: npm run build
  • Build RHF: npm run -w @nexcraft/forge-rhf build
  • Type-check both:
    • npm run type-check
    • npm run -w @nexcraft/forge-rhf type-check
  1. Next.js + Turbopack Interop (consumer guidance)
  • In consuming app next.config.mjs:
    • export default { transpilePackages: ['@nexcraft/forge', '@nexcraft/forge-rhf'] }
  • Usage:
    • import { ForgeInput } from '@nexcraft/forge/integrations/react'
    • import { RHFForgeInput } from '@nexcraft/forge-rhf'
  • Expectation:
    • Without RHF installed → importing @nexcraft/forge-rhf errors (expected).
    • With RHF installed → adapters render correctly.
  1. Documentation
  • Add docs/integrations/react-hook-form.md:
    • Install: npm i @nexcraft/forge @nexcraft/forge-rhf react-hook-form
    • Example with useForm + RHFForgeInput
    • Next.js note: transpilePackages
    • Rationale: no runtime detection; static imports only
  1. Publish
  • Publish RHF package when ready:
    • npm publish --workspace @nexcraft/forge-rhf
  • Keep base package publishing as-is.

Package Layouts

  • Base: @nexcraft/forge (unchanged outputs)
    • exports: "./integrations/react"dist/integrations/react/index.js|.d.ts
  • New: @nexcraft/forge-rhf
    • dist/index.js|.d.ts only; all adapters exported from root.

Acceptance Criteria

  • No require/eval/global detection in browser code.
  • @nexcraft/forge builds without RHF present; no RHF symbols exported.
  • @nexcraft/forge-rhf builds and type-checks; adapters compile against Forge React wrappers.
  • Next.js + Turbopack app can import both packages using transpilePackages.
  • Docs clearly state RHF as a peer dependency of the RHF package.

Risks & Mitigations

  • Turbopack symlink/module resolution → Use ESM only; document transpilePackages.
  • Peer dependency friction → Dedicated @nexcraft/forge-rhf keeps RHF optional and explicit.
  • Type drift between packages → CI/type-check both workspaces; keep adapters thin.

Notes:

  • Prefer exports as the single source of truth; avoid mixing main/module beyond pointing at the same ESM file.
  • Add 'use client' at entrypoints that use hooks if needed by consumers (usually not required in library code, but safe to add in adapter files if issues arise).

Monorepo Strategy & Publishing

See Phase 13 for the full monorepo plan and publishing workflows:

  • Phase 13: Monorepo Platform & Publishing — plans/phases/phase-13-monorepo-platform-and-publishing.md