From d9569cf7eff5cf50eadf050c67e9048cb4841e9b Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 14:11:41 -0500 Subject: [PATCH 01/11] fix(react-form): fix cache components support Attempts to resolve #1907 Removes `Math.random()` introduced in #1893 from the hot path allowing the page to be pre-built with a a user provided form id. source: https://nextjs.org/docs/app/getting-started/cache-components#non-deterministic-operations --- packages/react-form/src/useForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index bd7f4cb4f..316feeb87 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -184,7 +184,7 @@ export function useForm< TSubmitMeta >, ) { - const fallbackFormId = useState(() => uuid())[0] + const fallbackFormId = useState(() => opts?.formId ?? uuid())[0] const [prevFormId, setPrevFormId] = useState(opts?.formId as never) const [formApi, setFormApi] = useState(() => { From a5575e168aed9a426fdce9bc84821362582aae7e Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 14:16:58 -0500 Subject: [PATCH 02/11] generate changeset --- .changeset/metal-times-remain.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/metal-times-remain.md diff --git a/.changeset/metal-times-remain.md b/.changeset/metal-times-remain.md new file mode 100644 index 000000000..edd604b3a --- /dev/null +++ b/.changeset/metal-times-remain.md @@ -0,0 +1,9 @@ +--- +'@tanstack/react-form': major +'@tanstack/react-form-nextjs': major +--- + +uses the formId option by default as the initial fallback value, only calling Math.random() as a fallback if no formId is provided. + +- useId is not available in React 17, so in order for "cache components" to work the user must provide a deterministic formId (`useForm({ formId: 'my-form' })`). +- This enables static rendering as well as continued React 17 compatibility. From 2efacf57a9105ee5225f1d977fdeaba895f62c26 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 14:55:44 -0500 Subject: [PATCH 03/11] Change to patch --- .changeset/metal-times-remain.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/metal-times-remain.md b/.changeset/metal-times-remain.md index edd604b3a..296d57203 100644 --- a/.changeset/metal-times-remain.md +++ b/.changeset/metal-times-remain.md @@ -1,6 +1,6 @@ --- -'@tanstack/react-form': major -'@tanstack/react-form-nextjs': major +'@tanstack/react-form': patch +'@tanstack/react-form-nextjs': patch --- uses the formId option by default as the initial fallback value, only calling Math.random() as a fallback if no formId is provided. From cd6dc9376a46a717d8e8c9112dab202714624b3a Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 15:26:51 -0500 Subject: [PATCH 04/11] feat: use useId on React 18+ with a fallback to uuid on React 17 --- packages/react-form/src/useForm.tsx | 5 +++-- packages/react-form/src/useId.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 packages/react-form/src/useId.ts diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 316feeb87..e4f710bf2 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -1,10 +1,11 @@ 'use client' -import { FormApi, functionalUpdate, uuid } from '@tanstack/form-core' +import { FormApi, functionalUpdate } from '@tanstack/form-core' import { useStore } from '@tanstack/react-store' import { useMemo, useState } from 'react' import { Field } from './useField' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' +import { useId } from './useId' import type { AnyFormApi, AnyFormState, @@ -184,7 +185,7 @@ export function useForm< TSubmitMeta >, ) { - const fallbackFormId = useState(() => opts?.formId ?? uuid())[0] + const fallbackFormId = useId() const [prevFormId, setPrevFormId] = useState(opts?.formId as never) const [formApi, setFormApi] = useState(() => { diff --git a/packages/react-form/src/useId.ts b/packages/react-form/src/useId.ts new file mode 100644 index 000000000..c3f8af78c --- /dev/null +++ b/packages/react-form/src/useId.ts @@ -0,0 +1,13 @@ +import { version as reactVersion, useId as useReactId } from 'react' +import { uuid } from '@tanstack/form-core' + +/** React 17 does not have the useId hook, so we use a random uuid as a fallback. */ +export function useId() { + if (reactVersion.split('.')[0] === '17') { + return uuid() + } + + // react-compiler/react-compiler is disabled because useId is not available in React 17. However in React 18+ it is available and we want to use it. + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/rules-of-hooks + return useReactId() +} From 9ec39bbe5abe8d101efc1c555d6cb5994bf05c76 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 15:44:08 -0500 Subject: [PATCH 05/11] chore: incredible review feedback --- packages/react-form/src/useForm.tsx | 4 ++-- packages/react-form/src/useId.ts | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index e4f710bf2..47cdeecdb 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -5,7 +5,7 @@ import { useStore } from '@tanstack/react-store' import { useMemo, useState } from 'react' import { Field } from './useField' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' -import { useId } from './useId' +import { useFormId } from './useId' import type { AnyFormApi, AnyFormState, @@ -185,7 +185,7 @@ export function useForm< TSubmitMeta >, ) { - const fallbackFormId = useId() + const fallbackFormId = useFormId() const [prevFormId, setPrevFormId] = useState(opts?.formId as never) const [formApi, setFormApi] = useState(() => { diff --git a/packages/react-form/src/useId.ts b/packages/react-form/src/useId.ts index c3f8af78c..2ca494dbe 100644 --- a/packages/react-form/src/useId.ts +++ b/packages/react-form/src/useId.ts @@ -2,12 +2,4 @@ import { version as reactVersion, useId as useReactId } from 'react' import { uuid } from '@tanstack/form-core' /** React 17 does not have the useId hook, so we use a random uuid as a fallback. */ -export function useId() { - if (reactVersion.split('.')[0] === '17') { - return uuid() - } - - // react-compiler/react-compiler is disabled because useId is not available in React 17. However in React 18+ it is available and we want to use it. - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/rules-of-hooks - return useReactId() -} +export const useFormId = reactVersion.split('.')[0] === '17' ? uuid : useReactId From 63b56321e06185b5070b1a3ae9814a518740e5f1 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 15:47:19 -0500 Subject: [PATCH 06/11] update changelog --- .changeset/metal-times-remain.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.changeset/metal-times-remain.md b/.changeset/metal-times-remain.md index 296d57203..3deee1963 100644 --- a/.changeset/metal-times-remain.md +++ b/.changeset/metal-times-remain.md @@ -3,7 +3,4 @@ '@tanstack/react-form-nextjs': patch --- -uses the formId option by default as the initial fallback value, only calling Math.random() as a fallback if no formId is provided. - -- useId is not available in React 17, so in order for "cache components" to work the user must provide a deterministic formId (`useForm({ formId: 'my-form' })`). -- This enables static rendering as well as continued React 17 compatibility. +use React 18's useId hook by default for formId generation, only calling Math.random() as a fallback if no formId is provided. From a14b433f7a62a570e9d610d1f902ea44d6a9d3fe Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 15:47:46 -0500 Subject: [PATCH 07/11] chore: rename file --- packages/react-form/src/useForm.tsx | 2 +- packages/react-form/src/{useId.ts => useFormId.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/react-form/src/{useId.ts => useFormId.ts} (100%) diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 47cdeecdb..5abaa0351 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -5,7 +5,7 @@ import { useStore } from '@tanstack/react-store' import { useMemo, useState } from 'react' import { Field } from './useField' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' -import { useFormId } from './useId' +import { useFormId } from './useFormId' import type { AnyFormApi, AnyFormState, diff --git a/packages/react-form/src/useId.ts b/packages/react-form/src/useFormId.ts similarity index 100% rename from packages/react-form/src/useId.ts rename to packages/react-form/src/useFormId.ts From 34b41e6a65a3ba56c6fb44ec2802a81db757f9c9 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 17:25:04 -0500 Subject: [PATCH 08/11] import correctly --- packages/react-form/src/useFormId.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-form/src/useFormId.ts b/packages/react-form/src/useFormId.ts index 2ca494dbe..d73bd98a8 100644 --- a/packages/react-form/src/useFormId.ts +++ b/packages/react-form/src/useFormId.ts @@ -1,5 +1,5 @@ -import { version as reactVersion, useId as useReactId } from 'react' +import * as React from 'react' import { uuid } from '@tanstack/form-core' /** React 17 does not have the useId hook, so we use a random uuid as a fallback. */ -export const useFormId = reactVersion.split('.')[0] === '17' ? uuid : useReactId +export const useFormId = React.version.split('.')[0] === '17' ? uuid : React.useId From cba3df4f8cdd8059c0d8ab4d18b4858847a48b73 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 17:33:19 -0500 Subject: [PATCH 09/11] return a stable UUID reference --- packages/react-form/src/useFormId.ts | 4 ++-- packages/react-form/src/useUUID.ts | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 packages/react-form/src/useUUID.ts diff --git a/packages/react-form/src/useFormId.ts b/packages/react-form/src/useFormId.ts index d73bd98a8..178c74f63 100644 --- a/packages/react-form/src/useFormId.ts +++ b/packages/react-form/src/useFormId.ts @@ -1,5 +1,5 @@ import * as React from 'react' -import { uuid } from '@tanstack/form-core' +import { useUUID } from './useUUID' /** React 17 does not have the useId hook, so we use a random uuid as a fallback. */ -export const useFormId = React.version.split('.')[0] === '17' ? uuid : React.useId +export const useFormId = React.version.split('.')[0] === '17' ? useUUID : React.useId diff --git a/packages/react-form/src/useUUID.ts b/packages/react-form/src/useUUID.ts new file mode 100644 index 000000000..13b06d077 --- /dev/null +++ b/packages/react-form/src/useUUID.ts @@ -0,0 +1,7 @@ +import { useMemo } from "react" +import { uuid } from "@tanstack/form-core" + +/** Generates a random UUID. and returns a stable reference to it. */ +export function useUUID() { + return useMemo(() => uuid(), []) +} From 1601c6a35a5c9761dcae3631ece95a39277b9677 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sat, 6 Dec 2025 17:36:12 -0500 Subject: [PATCH 10/11] chore: switch from useMemo to useState --- packages/react-form/src/useUUID.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-form/src/useUUID.ts b/packages/react-form/src/useUUID.ts index 13b06d077..63482329a 100644 --- a/packages/react-form/src/useUUID.ts +++ b/packages/react-form/src/useUUID.ts @@ -1,7 +1,7 @@ -import { useMemo } from "react" +import { useState } from "react" import { uuid } from "@tanstack/form-core" /** Generates a random UUID. and returns a stable reference to it. */ export function useUUID() { - return useMemo(() => uuid(), []) + return useState(() => uuid())[0] } From 7546228bec9a9c5dd975a3f81197338b4604c735 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:46:36 +0000 Subject: [PATCH 11/11] ci: apply automated fixes and generate docs --- packages/react-form/src/useFormId.ts | 3 ++- packages/react-form/src/useUUID.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-form/src/useFormId.ts b/packages/react-form/src/useFormId.ts index 178c74f63..4b78db9b9 100644 --- a/packages/react-form/src/useFormId.ts +++ b/packages/react-form/src/useFormId.ts @@ -2,4 +2,5 @@ import * as React from 'react' import { useUUID } from './useUUID' /** React 17 does not have the useId hook, so we use a random uuid as a fallback. */ -export const useFormId = React.version.split('.')[0] === '17' ? useUUID : React.useId +export const useFormId = + React.version.split('.')[0] === '17' ? useUUID : React.useId diff --git a/packages/react-form/src/useUUID.ts b/packages/react-form/src/useUUID.ts index 63482329a..5dcfdb132 100644 --- a/packages/react-form/src/useUUID.ts +++ b/packages/react-form/src/useUUID.ts @@ -1,5 +1,5 @@ -import { useState } from "react" -import { uuid } from "@tanstack/form-core" +import { useState } from 'react' +import { uuid } from '@tanstack/form-core' /** Generates a random UUID. and returns a stable reference to it. */ export function useUUID() {