diff --git a/.changeset/react-router-v8-support.md b/.changeset/react-router-v8-support.md new file mode 100644 index 00000000..d409f5e3 --- /dev/null +++ b/.changeset/react-router-v8-support.md @@ -0,0 +1,5 @@ +--- +"@lambdacurry/forms": minor +--- + +Support React Router v8. `react-router` is now a peer dependency (`^7.0.0 || ^8.0.0`) instead of a bundled dependency, so the library resolves the router your application installs (keep the Vite `resolve.dedupe` / `ssr.noExternal` setup from the consumer guide so only one runtime instance is bundled), and the unused `react-router-dom` dependency/peer was dropped (that package no longer exists in React Router v8). React Router v8 requires Node 22.22 or newer. diff --git a/AGENTS.md b/AGENTS.md index 901e7159..862efd94 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,4 +81,4 @@ Quick checklist - Forms: Zod schemas, proper messages, `fetcher.Form`, show `FormMessage` errors. - Tests: per-story decorators, semantic queries, three-phase play tests; run `yarn test`. - Monorepo: no cross-package relative imports; verify `exports`, TS `paths`, Turbo outputs. -- Consumer integration: For React Router v7 setup help, reference `docs/consumer-setup-guide.md` for Vite SSR configuration (ssr.noExternal, optimizeDeps). +- Consumer integration: For React Router v7/v8 setup help, reference `docs/consumer-setup-guide.md` for Vite SSR configuration (ssr.noExternal, optimizeDeps). diff --git a/README.md b/README.md index 73da7016..830bf97c 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,11 @@ const MyTable = () => { - Full accessibility support (WCAG 2.1 AA) - Comprehensive test coverage -## React Router v7 Integration +## React Router v7 / v8 Integration -When using `@lambdacurry/forms` with `remix-hook-form` in a React Router v7 application, you need to configure Vite to bundle these packages together to share the router context. Without this, you may encounter the error: +`react`, `react-router` (v7 or v8), `remix-hook-form` and `zod` are peer dependencies: install them in your application so the library resolves the router (and React) your app provides. React Router v8 itself requires Node 22.22 or newer in the consuming application. + +When using `@lambdacurry/forms` with `remix-hook-form` in a React Router v7 or v8 application, you need to configure Vite to bundle these packages together to share the router context. Without this, you may encounter the error: ``` Error: useHref() may be used only in the context of a component. @@ -82,9 +84,11 @@ export default defineConfig({ ssr: { noExternal: ['react-hook-form', 'remix-hook-form', '@lambdacurry/forms'] }, - optimizeDeps: { - include: ['react', 'react-dom', 'react-router', 'react-hook-form', 'remix-hook-form'], + resolve: { dedupe: ['react', 'react-dom', 'react-router', 'react-hook-form', 'remix-hook-form'] + }, + optimizeDeps: { + include: ['react', 'react-dom', 'react-router', 'react-hook-form', 'remix-hook-form'] } }); ``` diff --git a/docs/consumer-setup-guide.md b/docs/consumer-setup-guide.md index 39ab4269..270a8c46 100644 --- a/docs/consumer-setup-guide.md +++ b/docs/consumer-setup-guide.md @@ -1,10 +1,12 @@ # Consumer Setup Guide -This guide covers how to integrate `@lambdacurry/forms` with React Router v7 applications using remix-hook-form. +This guide covers how to integrate `@lambdacurry/forms` with React Router v7 or v8 applications using remix-hook-form. -## React Router v7 Vite Configuration +`@lambdacurry/forms` declares four peer dependencies that your application must install: `react` (`^19.0.0`), `react-router` (`^7.0.0 || ^8.0.0`), `remix-hook-form` (`7.1.0`) and `zod` (`^3.24.1 || ^4.0.0`). The package no longer references `react-router-dom`, which was removed in React Router v8. React Router v8 requires Node 22.22 or newer. -When using `@lambdacurry/forms` with `remix-hook-form` in a React Router v7 application, you must configure Vite to bundle these packages together. Without this configuration, forms that render conditionally (e.g., triggered by a button click) will fail with: +## React Router Vite Configuration + +When using `@lambdacurry/forms` with `remix-hook-form` in a React Router v7 or v8 application, you must configure Vite to bundle these packages together. Without this configuration, forms that render conditionally (e.g., triggered by a button click) will fail with: ``` Error: useHref() may be used only in the context of a component. @@ -32,11 +34,13 @@ export default defineConfig({ // CRITICAL: Bundle these packages with the app to share react-router context noExternal: ['react-hook-form', 'remix-hook-form', '@lambdacurry/forms'] }, - optimizeDeps: { - // Pre-bundle dependencies to avoid runtime context issues - include: ['react', 'react-dom', 'react-router', 'react-hook-form', 'remix-hook-form'], + resolve: { // Ensure single instances of these packages dedupe: ['react', 'react-dom', 'react-router', 'react-hook-form', 'remix-hook-form'] + }, + optimizeDeps: { + // Pre-bundle dependencies to avoid runtime context issues + include: ['react', 'react-dom', 'react-router', 'react-hook-form', 'remix-hook-form'] } }); ``` @@ -47,7 +51,7 @@ export default defineConfig({ |---------|---------| | `ssr.noExternal` | Forces Vite to bundle `remix-hook-form`, `react-hook-form`, and `@lambdacurry/forms` with the application instead of treating them as external dependencies. This ensures they share the same `react-router` instance. | | `optimizeDeps.include` | Pre-bundles these packages during dev, avoiding lazy loading that can cause context issues. | -| `optimizeDeps.dedupe` | Ensures only one copy of each package exists, preventing multiple React or react-router instances. | +| `resolve.dedupe` | Ensures only one copy of each package exists, preventing multiple React or react-router instances. | ## Recommended Form Pattern @@ -126,7 +130,7 @@ function MyForm({ onSuccess }: { onSuccess: () => void }) { **Cause**: Vite is treating `remix-hook-form` or `react-hook-form` as external dependencies, causing them to load with a separate `react-router` instance. -**Solution**: Add the `ssr.noExternal` and `optimizeDeps` configuration shown above. +**Solution**: Add the `ssr.noExternal`, `resolve.dedupe` and `optimizeDeps.include` configuration shown above. ### Form works on initial render but fails when opened dynamically @@ -138,7 +142,7 @@ function MyForm({ onSuccess }: { onSuccess: () => void }) { **Cause**: Dependencies are being duplicated in the bundle. -**Solution**: Add `optimizeDeps.dedupe` with React and related packages. +**Solution**: Add `resolve.dedupe` with React and related packages, as in the configuration above. ## Related Documentation diff --git a/packages/components/package.json b/packages/components/package.json index d49eb4bd..7bce69d4 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -42,8 +42,7 @@ }, "peerDependencies": { "react": "^19.0.0", - "react-router": "^7.0.0", - "react-router-dom": "^7.0.0", + "react-router": "^7.0.0 || ^8.0.0", "remix-hook-form": "7.1.0", "zod": "^3.24.1 || ^4.0.0" }, @@ -76,8 +75,6 @@ "next-themes": "^0.4.4", "react-day-picker": "^9.7.0", "react-hook-form": "^7.53.1", - "react-router": "^7.6.3", - "react-router-dom": "^7.6.3", "remix-hook-form": "7.1.0", "sonner": "^1.7.1", "tailwind-merge": "^2.5.5", @@ -94,6 +91,7 @@ "autoprefixer": "^10.4.20", "glob": "^11.0.0", "react": "^19.0.0", + "react-router": "^7.6.3", "tailwindcss": "^4.0.0", "typescript": "^5.7.2", "vite": "^6.2.2", diff --git a/packages/components/vite.config.ts b/packages/components/vite.config.ts index d53bd131..9b15422c 100644 --- a/packages/components/vite.config.ts +++ b/packages/components/vite.config.ts @@ -62,7 +62,6 @@ export default defineConfig({ '@radix-ui/react-switch', '@radix-ui/react-tooltip', 'react-router', - 'react-router-dom', '@react-router/node', 'class-variance-authority', 'clsx', diff --git a/scripts/release-if-needed.mjs b/scripts/release-if-needed.mjs index 0deaafa8..a70bf422 100644 --- a/scripts/release-if-needed.mjs +++ b/scripts/release-if-needed.mjs @@ -1,17 +1,16 @@ -import { execFileSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; -const publishablePackages = ["packages/components/package.json"]; +const publishablePackages = ['packages/components/package.json']; const unpublishedPackages = publishablePackages.filter((packagePath) => { - const localPackage = JSON.parse(readFileSync(packagePath, "utf8")); + const localPackage = JSON.parse(readFileSync(packagePath, 'utf8')); try { - const exactVersion = execFileSync( - "npm", - ["view", `${localPackage.name}@${localPackage.version}`, "version"], - { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }, - ).trim(); + const exactVersion = execFileSync('npm', ['view', `${localPackage.name}@${localPackage.version}`, 'version'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); return exactVersion !== localPackage.version; } catch { @@ -20,8 +19,8 @@ const unpublishedPackages = publishablePackages.filter((packagePath) => { }); if (unpublishedPackages.length === 0) { - console.log("All publishable package versions are already on npm."); + console.log('All publishable package versions are already on npm.'); process.exit(0); } -execFileSync("yarn", ["changeset", "publish"], { stdio: "inherit" }); +execFileSync('yarn', ['changeset', 'publish'], { stdio: 'inherit' }); diff --git a/yarn.lock b/yarn.lock index 8a0053a1..14e8b0df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1754,7 +1754,6 @@ __metadata: react-day-picker: "npm:^9.7.0" react-hook-form: "npm:^7.53.1" react-router: "npm:^7.6.3" - react-router-dom: "npm:^7.6.3" remix-hook-form: "npm:7.1.0" sonner: "npm:^1.7.1" tailwind-merge: "npm:^2.5.5" @@ -1767,8 +1766,7 @@ __metadata: zod: "npm:^3.24.1" peerDependencies: react: ^19.0.0 - react-router: ^7.0.0 - react-router-dom: ^7.0.0 + react-router: ^7.0.0 || ^8.0.0 remix-hook-form: 7.1.0 zod: ^3.24.1 || ^4.0.0 languageName: unknown @@ -10410,7 +10408,7 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:^7.6.2, react-router-dom@npm:^7.6.3": +"react-router-dom@npm:^7.6.2": version: 7.9.1 resolution: "react-router-dom@npm:7.9.1" dependencies: