From 25f569fdf7be888de68a7ecae9403d41e23dfe0d Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Wed, 12 Nov 2025 21:07:08 +0100 Subject: [PATCH 1/6] fix: remove useId for react 17 compatibility --- .changeset/witty-chefs-greet.md | 5 +++++ packages/react-form/src/useForm.tsx | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/witty-chefs-greet.md diff --git a/.changeset/witty-chefs-greet.md b/.changeset/witty-chefs-greet.md new file mode 100644 index 000000000..7524b189e --- /dev/null +++ b/.changeset/witty-chefs-greet.md @@ -0,0 +1,5 @@ +--- +'@tanstack/react-form': patch +--- + +Remove useId for react 17 user compatibility, replaced with uuid diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index f9e725974..02831feb9 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -1,8 +1,8 @@ 'use client' -import { FormApi, functionalUpdate } from '@tanstack/form-core' +import { FormApi, functionalUpdate, uuid } from '@tanstack/form-core' import { useStore } from '@tanstack/react-store' -import { useId, useState } from 'react' +import { useMemo, useState } from 'react' import { Field } from './useField' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' import type { @@ -184,7 +184,7 @@ export function useForm< TSubmitMeta >, ) { - const formId = useId() + const formId = useMemo(() => uuid(), []) const [formApi] = useState(() => { const api = new FormApi< From 898d3e94502aca7059c4a44cc26093f745fce4f4 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Wed, 12 Nov 2025 21:18:32 +0100 Subject: [PATCH 2/6] fix: passthrough user defined formId --- packages/react-form/src/useForm.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 02831feb9..18e4b53ac 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -2,7 +2,7 @@ import { FormApi, functionalUpdate, uuid } from '@tanstack/form-core' import { useStore } from '@tanstack/react-store' -import { useMemo, useState } from 'react' +import { useMemo, useRef, useState } from 'react' import { Field } from './useField' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' import type { @@ -184,7 +184,8 @@ export function useForm< TSubmitMeta >, ) { - const formId = useMemo(() => uuid(), []) + const formIdRef = useRef(uuid()) + const formId = opts?.formId ?? formIdRef.current const [formApi] = useState(() => { const api = new FormApi< From 08ee36aa0f8e1dd1fa64537c70861c4501738c76 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Thu, 13 Nov 2025 10:15:06 +0100 Subject: [PATCH 3/6] chore: clean up --- packages/react-form/src/useForm.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 18e4b53ac..9c6f59171 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -184,8 +184,7 @@ export function useForm< TSubmitMeta >, ) { - const formIdRef = useRef(uuid()) - const formId = opts?.formId ?? formIdRef.current + const formId = useRef(opts?.formId ?? uuid()) const [formApi] = useState(() => { const api = new FormApi< @@ -201,7 +200,7 @@ export function useForm< TOnDynamicAsync, TOnServer, TSubmitMeta - >({ ...opts, formId: formId }) + >({ ...opts, formId: formId.current }) const extendedApi: ReactFormExtendedApi< TFormData, From de7136f48fa01584005aba69ac02d41fa2596fb1 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:12:14 +0100 Subject: [PATCH 4/6] fix: adjust component types to be React 17-19 compliant --- packages/react-form/src/createFormHook.tsx | 29 +++++++++++----------- packages/react-form/src/useField.tsx | 8 +++--- packages/react-form/src/useFieldGroup.tsx | 10 ++++---- packages/react-form/src/useForm.tsx | 8 +++--- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/react-form/src/createFormHook.tsx b/packages/react-form/src/createFormHook.tsx index 6f6e2d06f..ff8350231 100644 --- a/packages/react-form/src/createFormHook.tsx +++ b/packages/react-form/src/createFormHook.tsx @@ -17,8 +17,8 @@ import type { import type { ComponentType, Context, + FunctionComponent, PropsWithChildren, - ReactNode, } from 'react' import type { FieldComponent } from './useField' import type { ReactFormExtendedApi } from './useForm' @@ -191,7 +191,10 @@ export type AppFieldExtendedReactFormApi< TSubmitMeta, NoInfer > - AppForm: ComponentType + AppForm: ComponentType< + // PropsWithChildren

is not optional in React 17 + PropsWithChildren<{}> + > } export interface WithFormProps< @@ -226,8 +229,7 @@ export interface WithFormProps< > { // Optional, but adds props to the `render` function outside of `form` props?: TRenderProps - render: ( - props: PropsWithChildren< + render: FunctionComponent & { form: AppFieldExtendedReactFormApi< TFormData, @@ -246,8 +248,7 @@ export interface WithFormProps< TFormComponents > } - >, - ) => ReactNode + >> } export interface WithFieldGroupProps< @@ -259,8 +260,7 @@ export interface WithFieldGroupProps< > extends BaseFormOptions { // Optional, but adds props to the `render` function outside of `form` props?: TRenderProps - render: ( - props: PropsWithChildren< + render: FunctionComponent & { group: AppFieldExtendedReactFieldGroupApi< unknown, @@ -283,8 +283,7 @@ export interface WithFieldGroupProps< TFormComponents > } - >, - ) => ReactNode + >> } export function createFormHook< @@ -342,13 +341,13 @@ export function createFormHook< > { const form = useForm(props) - const AppForm = useMemo(() => { - const AppForm = (({ children }) => { + // PropsWithChildren

is not optional in React 17 + const AppForm = useMemo>>(() => { + return (({ children }) => { return ( {children} ) - }) as ComponentType - return AppForm + }) }, [form]) const AppField = useMemo(() => { @@ -521,7 +520,7 @@ export function createFormHook< fields: TFields } >, - ) => ReactNode { + ) => ReturnType { return function Render(innerProps) { const fieldGroupProps = useMemo(() => { return { diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index 0167e58aa..ced50e2a7 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -13,7 +13,7 @@ import type { FormAsyncValidateOrFn, FormValidateOrFn, } from '@tanstack/form-core' -import type { FunctionComponent, ReactNode } from 'react' +import type { FunctionComponent, ReactElement, ReactNode } from 'react' import type { UseFieldOptions, UseFieldOptionsBound } from './types' interface ReactFieldApi< @@ -496,7 +496,7 @@ export type FieldComponent< TFormOnServer, TPatentSubmitMeta, ExtendedApi ->) => ReactNode +>) => ReturnType /** * A type alias representing a field component for a form lens data type. @@ -584,7 +584,7 @@ export type LensFieldComponent< */ onBlurListenTo?: DeepKeys[] } -}) => ReactNode +}) => ReturnType /** * A function component that takes field options and a render function as children and returns a React component. @@ -650,7 +650,7 @@ export const Field = (< TFormOnDynamicAsync, TFormOnServer, TPatentSubmitMeta ->): ReactNode => { +>): ReturnType => { const fieldApi = useField(fieldOptions as any) const jsxToDisplay = useMemo( diff --git a/packages/react-form/src/useFieldGroup.tsx b/packages/react-form/src/useFieldGroup.tsx index 402a17107..cd231d7bb 100644 --- a/packages/react-form/src/useFieldGroup.tsx +++ b/packages/react-form/src/useFieldGroup.tsx @@ -13,7 +13,7 @@ import type { FormValidateOrFn, } from '@tanstack/form-core' import type { AppFieldExtendedReactFormApi } from './createFormHook' -import type { ComponentType, PropsWithChildren, ReactNode } from 'react' +import type { ComponentType, FunctionComponent, PropsWithChildren, ReactNode } from 'react' import type { LensFieldComponent } from './useField' function LocalSubscribe({ @@ -23,10 +23,10 @@ function LocalSubscribe({ }: PropsWithChildren<{ lens: AnyFieldGroupApi selector: (state: FieldGroupState) => FieldGroupState -}>) { +}>): ReturnType { const data = useStore(lens.store, selector) - return functionalUpdate(children, data) + return <>{functionalUpdate(children, data)} } /** @@ -73,7 +73,7 @@ export type AppFieldExtendedReactFieldGroupApi< TSubmitMeta, NoInfer > - AppForm: ComponentType + AppForm: ComponentType> /** * A React component to render form fields. With this, you can render and manage individual form fields. */ @@ -85,7 +85,7 @@ export type AppFieldExtendedReactFieldGroupApi< Subscribe: >>(props: { selector?: (state: NoInfer>) => TSelected children: ((state: NoInfer) => ReactNode) | ReactNode - }) => ReactNode + }) => ReturnType } export function useFieldGroup< diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 9c6f59171..0b42c7ba5 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -13,7 +13,7 @@ import type { FormState, FormValidateOrFn, } from '@tanstack/form-core' -import type { PropsWithChildren, ReactNode } from 'react' +import type { FunctionComponent, PropsWithChildren, ReactElement, ReactNode } from 'react' import type { FieldComponent } from './useField' import type { NoInfer } from '@tanstack/react-store' @@ -89,7 +89,7 @@ export interface ReactFormApi< >, ) => TSelected children: ((state: NoInfer) => ReactNode) | ReactNode - }) => ReactNode + }) => ReturnType } /** @@ -144,10 +144,10 @@ function LocalSubscribe({ }: PropsWithChildren<{ form: AnyFormApi selector: (state: AnyFormState) => AnyFormState -}>) { +}>): ReturnType { const data = useStore(form.store, selector) - return functionalUpdate(children, data) + return <>{functionalUpdate(children, data)} } /** From 040e62f54985ecbc48e7bf11b62cafc00a3b2526 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:15:56 +0000 Subject: [PATCH 5/6] ci: apply automated fixes and generate docs --- packages/react-form/src/createFormHook.tsx | 20 ++++++++++++-------- packages/react-form/src/useFieldGroup.tsx | 7 ++++++- packages/react-form/src/useForm.tsx | 7 ++++++- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/react-form/src/createFormHook.tsx b/packages/react-form/src/createFormHook.tsx index ff8350231..1f44b171d 100644 --- a/packages/react-form/src/createFormHook.tsx +++ b/packages/react-form/src/createFormHook.tsx @@ -192,8 +192,8 @@ export type AppFieldExtendedReactFormApi< NoInfer > AppForm: ComponentType< - // PropsWithChildren

is not optional in React 17 - PropsWithChildren<{}> + // PropsWithChildren

is not optional in React 17 + PropsWithChildren<{}> > } @@ -229,7 +229,8 @@ export interface WithFormProps< > { // Optional, but adds props to the `render` function outside of `form` props?: TRenderProps - render: FunctionComponent & { form: AppFieldExtendedReactFormApi< TFormData, @@ -248,7 +249,8 @@ export interface WithFormProps< TFormComponents > } - >> + > + > } export interface WithFieldGroupProps< @@ -260,7 +262,8 @@ export interface WithFieldGroupProps< > extends BaseFormOptions { // Optional, but adds props to the `render` function outside of `form` props?: TRenderProps - render: FunctionComponent & { group: AppFieldExtendedReactFieldGroupApi< unknown, @@ -283,7 +286,8 @@ export interface WithFieldGroupProps< TFormComponents > } - >> + > + > } export function createFormHook< @@ -343,11 +347,11 @@ export function createFormHook< // PropsWithChildren

is not optional in React 17 const AppForm = useMemo>>(() => { - return (({ children }) => { + return ({ children }) => { return ( {children} ) - }) + } }, [form]) const AppField = useMemo(() => { diff --git a/packages/react-form/src/useFieldGroup.tsx b/packages/react-form/src/useFieldGroup.tsx index cd231d7bb..063187c52 100644 --- a/packages/react-form/src/useFieldGroup.tsx +++ b/packages/react-form/src/useFieldGroup.tsx @@ -13,7 +13,12 @@ import type { FormValidateOrFn, } from '@tanstack/form-core' import type { AppFieldExtendedReactFormApi } from './createFormHook' -import type { ComponentType, FunctionComponent, PropsWithChildren, ReactNode } from 'react' +import type { + ComponentType, + FunctionComponent, + PropsWithChildren, + ReactNode, +} from 'react' import type { LensFieldComponent } from './useField' function LocalSubscribe({ diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 0b42c7ba5..2a019ea1a 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -13,7 +13,12 @@ import type { FormState, FormValidateOrFn, } from '@tanstack/form-core' -import type { FunctionComponent, PropsWithChildren, ReactElement, ReactNode } from 'react' +import type { + FunctionComponent, + PropsWithChildren, + ReactElement, + ReactNode, +} from 'react' import type { FieldComponent } from './useField' import type { NoInfer } from '@tanstack/react-store' From b7cc7cbf42bb8d7028f884162e73d87870029bd0 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Thu, 27 Nov 2025 12:56:21 +0100 Subject: [PATCH 6/6] chore: pr comments --- packages/react-form/src/useForm.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 2a019ea1a..3246b41a4 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -2,7 +2,7 @@ import { FormApi, functionalUpdate, uuid } from '@tanstack/form-core' import { useStore } from '@tanstack/react-store' -import { useMemo, useRef, useState } from 'react' +import { useRef, useState } from 'react' import { Field } from './useField' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' import type { @@ -189,7 +189,11 @@ export function useForm< TSubmitMeta >, ) { - const formId = useRef(opts?.formId ?? uuid()) + const formId = useRef(opts?.formId as never) + + if (!formId.current) { + formId.current = uuid() + } const [formApi] = useState(() => { const api = new FormApi<