@tanstack/react-form 1.26.0 → 1.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
3
- import { FormApi, functionalUpdate } from "@tanstack/form-core";
2
+ import { jsx, Fragment } from "react/jsx-runtime";
3
+ import { uuid, FormApi, functionalUpdate } from "@tanstack/form-core";
4
4
  import { useStore } from "@tanstack/react-store";
5
- import { useId, useState } from "react";
5
+ import { useState, useMemo } from "react";
6
6
  import { Field } from "./useField.js";
7
7
  import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
8
8
  function LocalSubscribe({
@@ -11,33 +11,53 @@ function LocalSubscribe({
11
11
  children
12
12
  }) {
13
13
  const data = useStore(form.store, selector);
14
- return functionalUpdate(children, data);
14
+ return /* @__PURE__ */ jsx(Fragment, { children: functionalUpdate(children, data) });
15
15
  }
16
16
  function useForm(opts) {
17
- const formId = useId();
18
- const [formApi] = useState(() => {
19
- const api = new FormApi({ ...opts, formId });
20
- const extendedApi = api;
17
+ const fallbackFormId = useState(() => uuid())[0];
18
+ const [prevFormId, setPrevFormId] = useState(opts?.formId);
19
+ const [formApi, setFormApi] = useState(() => {
20
+ return new FormApi({ ...opts, formId: opts?.formId ?? fallbackFormId });
21
+ });
22
+ if (prevFormId !== opts?.formId) {
23
+ const formId = opts?.formId ?? fallbackFormId;
24
+ setFormApi(new FormApi({ ...opts, formId }));
25
+ setPrevFormId(formId);
26
+ }
27
+ const extendedFormApi = useMemo(() => {
28
+ const extendedApi = {
29
+ ...formApi,
30
+ handleSubmit: ((...props) => {
31
+ formApi._handleSubmit(...props);
32
+ }),
33
+ // We must add all `get`ters from `core`'s `FormApi` here, as otherwise the spread operator won't catch those
34
+ get formId() {
35
+ return formApi._formId;
36
+ },
37
+ get state() {
38
+ return formApi.store.state;
39
+ }
40
+ };
21
41
  extendedApi.Field = function APIField(props) {
22
- return /* @__PURE__ */ jsx(Field, { ...props, form: api });
42
+ return /* @__PURE__ */ jsx(Field, { ...props, form: formApi });
23
43
  };
24
44
  extendedApi.Subscribe = function Subscribe(props) {
25
45
  return /* @__PURE__ */ jsx(
26
46
  LocalSubscribe,
27
47
  {
28
- form: api,
48
+ form: formApi,
29
49
  selector: props.selector,
30
50
  children: props.children
31
51
  }
32
52
  );
33
53
  };
34
54
  return extendedApi;
35
- });
55
+ }, [formApi]);
36
56
  useIsomorphicLayoutEffect(formApi.mount, []);
37
57
  useIsomorphicLayoutEffect(() => {
38
58
  formApi.update(opts);
39
59
  });
40
- return formApi;
60
+ return extendedFormApi;
41
61
  }
42
62
  export {
43
63
  useForm
@@ -1 +1 @@
1
- {"version":3,"file":"useForm.js","sources":["../../src/useForm.tsx"],"sourcesContent":["'use client'\n\nimport { FormApi, functionalUpdate } from '@tanstack/form-core'\nimport { useStore } from '@tanstack/react-store'\nimport { useId, useState } from 'react'\nimport { Field } from './useField'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type {\n AnyFormApi,\n AnyFormState,\n FormAsyncValidateOrFn,\n FormOptions,\n FormState,\n FormValidateOrFn,\n} from '@tanstack/form-core'\nimport type { PropsWithChildren, ReactNode } from 'react'\nimport type { FieldComponent } from './useField'\nimport type { NoInfer } from '@tanstack/react-store'\n\n/**\n * Fields that are added onto the `FormAPI` from `@tanstack/form-core` and returned from `useForm`\n */\nexport interface ReactFormApi<\n in out TFormData,\n in out TOnMount extends undefined | FormValidateOrFn<TFormData>,\n in out TOnChange extends undefined | FormValidateOrFn<TFormData>,\n in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,\n in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,\n in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,\n in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TSubmitMeta,\n> {\n /**\n * A React component to render form fields. With this, you can render and manage individual form fields.\n */\n Field: FieldComponent<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >\n /**\n * A `Subscribe` function that allows you to listen and react to changes in the form's state. It's especially useful when you need to execute side effects or render specific components in response to state updates.\n */\n Subscribe: <\n TSelected = NoInfer<\n FormState<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer\n >\n >,\n >(props: {\n selector?: (\n state: NoInfer<\n FormState<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer\n >\n >,\n ) => TSelected\n children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode\n }) => ReactNode\n}\n\n/**\n * An extended version of the `FormApi` class that includes React-specific functionalities from `ReactFormApi`\n */\nexport type ReactFormExtendedApi<\n TFormData,\n TOnMount extends undefined | FormValidateOrFn<TFormData>,\n TOnChange extends undefined | FormValidateOrFn<TFormData>,\n TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnBlur extends undefined | FormValidateOrFn<TFormData>,\n TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnSubmit extends undefined | FormValidateOrFn<TFormData>,\n TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnDynamic extends undefined | FormValidateOrFn<TFormData>,\n TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,\n TSubmitMeta,\n> = FormApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n> &\n ReactFormApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >\n\nfunction LocalSubscribe({\n form,\n selector,\n children,\n}: PropsWithChildren<{\n form: AnyFormApi\n selector: (state: AnyFormState) => AnyFormState\n}>) {\n const data = useStore(form.store, selector)\n\n return functionalUpdate(children, data)\n}\n\n/**\n * A custom React Hook that returns an extended instance of the `FormApi` class.\n *\n * This API encapsulates all the necessary functionalities related to the form. It allows you to manage form state, handle submissions, and interact with form fields\n */\nexport function useForm<\n TFormData,\n TOnMount extends undefined | FormValidateOrFn<TFormData>,\n TOnChange extends undefined | FormValidateOrFn<TFormData>,\n TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnBlur extends undefined | FormValidateOrFn<TFormData>,\n TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnSubmit extends undefined | FormValidateOrFn<TFormData>,\n TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnDynamic extends undefined | FormValidateOrFn<TFormData>,\n TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,\n TSubmitMeta,\n>(\n opts?: FormOptions<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >,\n) {\n const formId = useId()\n\n const [formApi] = useState(() => {\n const api = new FormApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >({ ...opts, formId: formId })\n\n const extendedApi: ReactFormExtendedApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n > = api as never\n\n extendedApi.Field = function APIField(props) {\n return <Field {...props} form={api} />\n }\n\n extendedApi.Subscribe = function Subscribe(props: any) {\n return (\n <LocalSubscribe\n form={api}\n selector={props.selector}\n children={props.children}\n />\n )\n }\n\n return extendedApi\n })\n\n useIsomorphicLayoutEffect(formApi.mount, [])\n\n /**\n * formApi.update should not have any side effects. Think of it like a `useRef`\n * that we need to keep updated every render with the most up-to-date information.\n */\n useIsomorphicLayoutEffect(() => {\n formApi.update(opts)\n })\n\n return formApi\n}\n"],"names":[],"mappings":";;;;;;;AA2IA;AAAwB;AACtB;AACA;AAEF;AAIE;AAEA;AACF;AAOO;AA6BL;AAEA;AACE;AAeA;AAeA;AACE;AAAoC;AAGtC;AACE;AACE;AAAC;AAAA;AACO;AACU;AACA;AAAA;AAAA;AAKtB;AAAO;AAGT;AAMA;AACE;AAAmB;AAGrB;AACF;;;;"}
1
+ {"version":3,"file":"useForm.js","sources":["../../src/useForm.tsx"],"sourcesContent":["'use client'\n\nimport { FormApi, functionalUpdate, uuid } from '@tanstack/form-core'\nimport { useStore } from '@tanstack/react-store'\nimport { useMemo, useState } from 'react'\nimport { Field } from './useField'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type {\n AnyFormApi,\n AnyFormState,\n FormAsyncValidateOrFn,\n FormOptions,\n FormState,\n FormValidateOrFn,\n} from '@tanstack/form-core'\nimport type { FunctionComponent, PropsWithChildren, ReactNode } from 'react'\nimport type { FieldComponent } from './useField'\nimport type { NoInfer } from '@tanstack/react-store'\n\n/**\n * Fields that are added onto the `FormAPI` from `@tanstack/form-core` and returned from `useForm`\n */\nexport interface ReactFormApi<\n in out TFormData,\n in out TOnMount extends undefined | FormValidateOrFn<TFormData>,\n in out TOnChange extends undefined | FormValidateOrFn<TFormData>,\n in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,\n in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,\n in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,\n in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TSubmitMeta,\n> {\n /**\n * A React component to render form fields. With this, you can render and manage individual form fields.\n */\n Field: FieldComponent<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >\n /**\n * A `Subscribe` function that allows you to listen and react to changes in the form's state. It's especially useful when you need to execute side effects or render specific components in response to state updates.\n */\n Subscribe: <\n TSelected = NoInfer<\n FormState<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer\n >\n >,\n >(props: {\n selector?: (\n state: NoInfer<\n FormState<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer\n >\n >,\n ) => TSelected\n children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode\n }) => ReturnType<FunctionComponent>\n}\n\n/**\n * An extended version of the `FormApi` class that includes React-specific functionalities from `ReactFormApi`\n */\nexport type ReactFormExtendedApi<\n TFormData,\n TOnMount extends undefined | FormValidateOrFn<TFormData>,\n TOnChange extends undefined | FormValidateOrFn<TFormData>,\n TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnBlur extends undefined | FormValidateOrFn<TFormData>,\n TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnSubmit extends undefined | FormValidateOrFn<TFormData>,\n TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnDynamic extends undefined | FormValidateOrFn<TFormData>,\n TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,\n TSubmitMeta,\n> = FormApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n> &\n ReactFormApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >\n\nfunction LocalSubscribe({\n form,\n selector,\n children,\n}: PropsWithChildren<{\n form: AnyFormApi\n selector: (state: AnyFormState) => AnyFormState\n}>): ReturnType<FunctionComponent> {\n const data = useStore(form.store, selector)\n\n return <>{functionalUpdate(children, data)}</>\n}\n\n/**\n * A custom React Hook that returns an extended instance of the `FormApi` class.\n *\n * This API encapsulates all the necessary functionalities related to the form. It allows you to manage form state, handle submissions, and interact with form fields\n */\nexport function useForm<\n TFormData,\n TOnMount extends undefined | FormValidateOrFn<TFormData>,\n TOnChange extends undefined | FormValidateOrFn<TFormData>,\n TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnBlur extends undefined | FormValidateOrFn<TFormData>,\n TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnSubmit extends undefined | FormValidateOrFn<TFormData>,\n TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnDynamic extends undefined | FormValidateOrFn<TFormData>,\n TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,\n TSubmitMeta,\n>(\n opts?: FormOptions<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >,\n) {\n const fallbackFormId = useState(() => uuid())[0]\n const [prevFormId, setPrevFormId] = useState<string>(opts?.formId as never)\n\n const [formApi, setFormApi] = useState(() => {\n return new FormApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >({ ...opts, formId: opts?.formId ?? fallbackFormId })\n })\n\n if (prevFormId !== opts?.formId) {\n const formId = opts?.formId ?? fallbackFormId\n setFormApi(new FormApi({ ...opts, formId }))\n setPrevFormId(formId)\n }\n\n const extendedFormApi = useMemo(() => {\n const extendedApi: ReactFormExtendedApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n > = {\n ...formApi,\n handleSubmit: ((...props: never[]) => {\n formApi._handleSubmit(...props)\n }) as typeof formApi.handleSubmit,\n // We must add all `get`ters from `core`'s `FormApi` here, as otherwise the spread operator won't catch those\n get formId(): string {\n return formApi._formId\n },\n get state() {\n return formApi.store.state\n },\n } as never\n\n extendedApi.Field = function APIField(props) {\n return <Field {...props} form={formApi} />\n }\n\n extendedApi.Subscribe = function Subscribe(props: any) {\n return (\n <LocalSubscribe\n form={formApi}\n selector={props.selector}\n children={props.children}\n />\n )\n }\n\n return extendedApi\n }, [formApi])\n\n useIsomorphicLayoutEffect(formApi.mount, [])\n\n /**\n * formApi.update should not have any side effects. Think of it like a `useRef`\n * that we need to keep updated every render with the most up-to-date information.\n */\n useIsomorphicLayoutEffect(() => {\n formApi.update(opts)\n })\n\n return extendedFormApi\n}\n"],"names":[],"mappings":";;;;;;;AA2IA;AAAwB;AACtB;AACA;AAEF;AAIE;AAEA;AACF;AAOO;AA6BL;AACA;AAEA;AACE;AAaqD;AAGvD;AACE;AACA;AACA;AAAoB;AAGtB;AACE;AAaI;AACC;AAED;AAA8B;AAChC;AAAA;AAGE;AAAe;AACjB;AAEE;AAAqB;AACvB;AAGF;AACE;AAAwC;AAG1C;AACE;AACE;AAAC;AAAA;AACO;AACU;AACA;AAAA;AAAA;AAKtB;AAAO;AAGT;AAMA;AACE;AAAmB;AAGrB;AACF;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-form",
3
- "version": "1.26.0",
3
+ "version": "1.27.1",
4
4
  "description": "Powerful, type-safe forms for React.",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -37,8 +37,8 @@
37
37
  "src"
38
38
  ],
39
39
  "dependencies": {
40
- "@tanstack/react-store": "^0.7.7",
41
- "@tanstack/form-core": "1.26.0"
40
+ "@tanstack/react-store": "^0.8.0",
41
+ "@tanstack/form-core": "1.27.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/react": "^19.0.7",
@@ -17,8 +17,8 @@ import type {
17
17
  import type {
18
18
  ComponentType,
19
19
  Context,
20
+ FunctionComponent,
20
21
  PropsWithChildren,
21
- ReactNode,
22
22
  } from 'react'
23
23
  import type { FieldComponent } from './useField'
24
24
  import type { ReactFormExtendedApi } from './useForm'
@@ -191,7 +191,10 @@ export type AppFieldExtendedReactFormApi<
191
191
  TSubmitMeta,
192
192
  NoInfer<TFieldComponents>
193
193
  >
194
- AppForm: ComponentType<PropsWithChildren>
194
+ AppForm: ComponentType<
195
+ // PropsWithChildren<P> is not optional in React 17
196
+ PropsWithChildren<{}>
197
+ >
195
198
  }
196
199
 
197
200
  export interface WithFormProps<
@@ -226,8 +229,8 @@ export interface WithFormProps<
226
229
  > {
227
230
  // Optional, but adds props to the `render` function outside of `form`
228
231
  props?: TRenderProps
229
- render: (
230
- props: PropsWithChildren<
232
+ render: FunctionComponent<
233
+ PropsWithChildren<
231
234
  NoInfer<TRenderProps> & {
232
235
  form: AppFieldExtendedReactFormApi<
233
236
  TFormData,
@@ -246,8 +249,8 @@ export interface WithFormProps<
246
249
  TFormComponents
247
250
  >
248
251
  }
249
- >,
250
- ) => ReactNode
252
+ >
253
+ >
251
254
  }
252
255
 
253
256
  export interface WithFieldGroupProps<
@@ -259,8 +262,8 @@ export interface WithFieldGroupProps<
259
262
  > extends BaseFormOptions<TFieldGroupData, TSubmitMeta> {
260
263
  // Optional, but adds props to the `render` function outside of `form`
261
264
  props?: TRenderProps
262
- render: (
263
- props: PropsWithChildren<
265
+ render: FunctionComponent<
266
+ PropsWithChildren<
264
267
  NoInfer<TRenderProps> & {
265
268
  group: AppFieldExtendedReactFieldGroupApi<
266
269
  unknown,
@@ -283,8 +286,8 @@ export interface WithFieldGroupProps<
283
286
  TFormComponents
284
287
  >
285
288
  }
286
- >,
287
- ) => ReactNode
289
+ >
290
+ >
288
291
  }
289
292
 
290
293
  export function createFormHook<
@@ -342,13 +345,13 @@ export function createFormHook<
342
345
  > {
343
346
  const form = useForm(props)
344
347
 
345
- const AppForm = useMemo(() => {
346
- const AppForm = (({ children }) => {
348
+ // PropsWithChildren<P> is not optional in React 17
349
+ const AppForm = useMemo<ComponentType<PropsWithChildren<{}>>>(() => {
350
+ return ({ children }) => {
347
351
  return (
348
352
  <formContext.Provider value={form}>{children}</formContext.Provider>
349
353
  )
350
- }) as ComponentType<PropsWithChildren>
351
- return AppForm
354
+ }
352
355
  }, [form])
353
356
 
354
357
  const AppField = useMemo(() => {
@@ -521,7 +524,7 @@ export function createFormHook<
521
524
  fields: TFields
522
525
  }
523
526
  >,
524
- ) => ReactNode {
527
+ ) => ReturnType<FunctionComponent> {
525
528
  return function Render(innerProps) {
526
529
  const fieldGroupProps = useMemo(() => {
527
530
  return {
package/src/useField.tsx CHANGED
@@ -1,10 +1,12 @@
1
1
  'use client'
2
2
 
3
- import { useMemo, useRef } from 'react'
3
+ import { useMemo, useRef, useState } from 'react'
4
4
  import { useStore } from '@tanstack/react-store'
5
5
  import { FieldApi, functionalUpdate } from '@tanstack/form-core'
6
6
  import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
7
7
  import type {
8
+ AnyFieldApi,
9
+ AnyFieldMeta,
8
10
  DeepKeys,
9
11
  DeepValue,
10
12
  FieldAsyncValidateOrFn,
@@ -13,7 +15,7 @@ import type {
13
15
  FormAsyncValidateOrFn,
14
16
  FormValidateOrFn,
15
17
  } from '@tanstack/form-core'
16
- import type { FunctionComponent, ReactNode } from 'react'
18
+ import type { FunctionComponent, ReactElement, ReactNode } from 'react'
17
19
  import type { UseFieldOptions, UseFieldOptionsBound } from './types'
18
20
 
19
21
  interface ReactFieldApi<
@@ -195,17 +197,103 @@ export function useField<
195
197
  ) {
196
198
  // Keep a snapshot of options so that React Compiler doesn't
197
199
  // wrongly optimize fieldApi.
198
- const optsRef = useRef(opts)
199
- optsRef.current = opts
200
+ const [prevOptions, setPrevOptions] = useState(() => ({
201
+ form: opts.form,
202
+ name: opts.name,
203
+ }))
200
204
 
201
- const fieldApi = useMemo(() => {
202
- const api = new FieldApi({
203
- ...optsRef.current,
204
- form: opts.form,
205
- name: opts.name,
205
+ const [fieldApi, setFieldApi] = useState(() => {
206
+ return new FieldApi({
207
+ ...opts,
206
208
  })
209
+ })
210
+
211
+ // We only want to
212
+ // update on name changes since those are at risk of becoming stale. The field
213
+ // state must be up to date for the internal JSX render.
214
+ // The other options can freely be in `fieldApi.update`
215
+ if (prevOptions.form !== opts.form || prevOptions.name !== opts.name) {
216
+ setFieldApi(
217
+ new FieldApi({
218
+ ...opts,
219
+ }),
220
+ )
221
+ setPrevOptions({ form: opts.form, name: opts.name })
222
+ }
223
+
224
+ const reactiveStateValue = useStore(fieldApi.store, (state) => state.value)
225
+ const reactiveMetaIsTouched = useStore(
226
+ fieldApi.store,
227
+ (state) => state.meta.isTouched,
228
+ )
229
+ const reactiveMetaIsBlurred = useStore(
230
+ fieldApi.store,
231
+ (state) => state.meta.isBlurred,
232
+ )
233
+ const reactiveMetaIsDirty = useStore(
234
+ fieldApi.store,
235
+ (state) => state.meta.isDirty,
236
+ )
237
+ const reactiveMetaErrorMap = useStore(
238
+ fieldApi.store,
239
+ (state) => state.meta.errorMap,
240
+ )
241
+ const reactiveMetaErrorSourceMap = useStore(
242
+ fieldApi.store,
243
+ (state) => state.meta.errorSourceMap,
244
+ )
245
+ const reactiveMetaIsValidating = useStore(
246
+ fieldApi.store,
247
+ (state) => state.meta.isValidating,
248
+ )
249
+
250
+ // This makes me sad, but if I understand correctly, this is what we have to do for reactivity to work properly with React compiler.
251
+ const extendedFieldApi = useMemo(() => {
252
+ const reactiveFieldApi = {
253
+ ...fieldApi,
254
+ get state() {
255
+ return {
256
+ value: reactiveStateValue,
257
+ get meta() {
258
+ return {
259
+ ...fieldApi.state.meta,
260
+ isTouched: reactiveMetaIsTouched,
261
+ isBlurred: reactiveMetaIsBlurred,
262
+ isDirty: reactiveMetaIsDirty,
263
+ errorMap: reactiveMetaErrorMap,
264
+ errorSourceMap: reactiveMetaErrorSourceMap,
265
+ isValidating: reactiveMetaIsValidating,
266
+ } satisfies AnyFieldMeta
267
+ },
268
+ } satisfies AnyFieldApi['state']
269
+ },
270
+ }
207
271
 
208
- const extendedApi: typeof api &
272
+ const extendedApi: FieldApi<
273
+ TParentData,
274
+ TName,
275
+ TData,
276
+ TOnMount,
277
+ TOnChange,
278
+ TOnChangeAsync,
279
+ TOnBlur,
280
+ TOnBlurAsync,
281
+ TOnSubmit,
282
+ TOnSubmitAsync,
283
+ TOnDynamic,
284
+ TOnDynamicAsync,
285
+ TFormOnMount,
286
+ TFormOnChange,
287
+ TFormOnChangeAsync,
288
+ TFormOnBlur,
289
+ TFormOnBlurAsync,
290
+ TFormOnSubmit,
291
+ TFormOnSubmitAsync,
292
+ TFormOnDynamic,
293
+ TFormOnDynamicAsync,
294
+ TFormOnServer,
295
+ TPatentSubmitMeta
296
+ > &
209
297
  ReactFieldApi<
210
298
  TParentData,
211
299
  TFormOnMount,
@@ -219,16 +307,21 @@ export function useField<
219
307
  TFormOnDynamicAsync,
220
308
  TFormOnServer,
221
309
  TPatentSubmitMeta
222
- > = api as never
310
+ > = reactiveFieldApi as never
223
311
 
224
312
  extendedApi.Field = Field as never
225
313
 
226
314
  return extendedApi
227
- // We only want to
228
- // update on name changes since those are at risk of becoming stale. The field
229
- // state must be up to date for the internal JSX render.
230
- // The other options can freely be in `fieldApi.update`
231
- }, [opts.form, opts.name])
315
+ }, [
316
+ fieldApi,
317
+ reactiveStateValue,
318
+ reactiveMetaIsTouched,
319
+ reactiveMetaIsBlurred,
320
+ reactiveMetaIsDirty,
321
+ reactiveMetaErrorMap,
322
+ reactiveMetaErrorSourceMap,
323
+ reactiveMetaIsValidating,
324
+ ])
232
325
 
233
326
  useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi])
234
327
 
@@ -252,7 +345,7 @@ export function useField<
252
345
  : undefined,
253
346
  )
254
347
 
255
- return fieldApi
348
+ return extendedFieldApi
256
349
  }
257
350
 
258
351
  /**
@@ -496,7 +589,7 @@ export type FieldComponent<
496
589
  TFormOnServer,
497
590
  TPatentSubmitMeta,
498
591
  ExtendedApi
499
- >) => ReactNode
592
+ >) => ReturnType<FunctionComponent>
500
593
 
501
594
  /**
502
595
  * A type alias representing a field component for a form lens data type.
@@ -584,7 +677,7 @@ export type LensFieldComponent<
584
677
  */
585
678
  onBlurListenTo?: DeepKeys<TLensData>[]
586
679
  }
587
- }) => ReactNode
680
+ }) => ReturnType<FunctionComponent>
588
681
 
589
682
  /**
590
683
  * A function component that takes field options and a render function as children and returns a React component.
@@ -650,18 +743,12 @@ export const Field = (<
650
743
  TFormOnDynamicAsync,
651
744
  TFormOnServer,
652
745
  TPatentSubmitMeta
653
- >): ReactNode => {
746
+ >): ReturnType<FunctionComponent> => {
654
747
  const fieldApi = useField(fieldOptions as any)
655
748
 
656
749
  const jsxToDisplay = useMemo(
657
750
  () => functionalUpdate(children, fieldApi as any),
658
- /**
659
- * The reason this exists is to fix an issue with the React Compiler.
660
- * Namely, functionalUpdate is memoized where it checks for `fieldApi`, which is a static type.
661
- * This means that when `state.value` changes, it does not trigger a re-render. The useMemo explicitly fixes this problem
662
- */
663
- // eslint-disable-next-line react-hooks/exhaustive-deps
664
- [children, fieldApi, fieldApi.state.value, fieldApi.state.meta],
751
+ [children, fieldApi],
665
752
  )
666
753
  return (<>{jsxToDisplay}</>) as never
667
754
  }) satisfies FunctionComponent<
@@ -13,7 +13,12 @@ import type {
13
13
  FormValidateOrFn,
14
14
  } from '@tanstack/form-core'
15
15
  import type { AppFieldExtendedReactFormApi } from './createFormHook'
16
- import type { ComponentType, PropsWithChildren, ReactNode } from 'react'
16
+ import type {
17
+ ComponentType,
18
+ FunctionComponent,
19
+ PropsWithChildren,
20
+ ReactNode,
21
+ } from 'react'
17
22
  import type { LensFieldComponent } from './useField'
18
23
 
19
24
  function LocalSubscribe({
@@ -23,10 +28,10 @@ function LocalSubscribe({
23
28
  }: PropsWithChildren<{
24
29
  lens: AnyFieldGroupApi
25
30
  selector: (state: FieldGroupState<any>) => FieldGroupState<any>
26
- }>) {
31
+ }>): ReturnType<FunctionComponent> {
27
32
  const data = useStore(lens.store, selector)
28
33
 
29
- return functionalUpdate(children, data)
34
+ return <>{functionalUpdate(children, data)}</>
30
35
  }
31
36
 
32
37
  /**
@@ -73,7 +78,7 @@ export type AppFieldExtendedReactFieldGroupApi<
73
78
  TSubmitMeta,
74
79
  NoInfer<TFieldComponents>
75
80
  >
76
- AppForm: ComponentType<PropsWithChildren>
81
+ AppForm: ComponentType<PropsWithChildren<{}>>
77
82
  /**
78
83
  * A React component to render form fields. With this, you can render and manage individual form fields.
79
84
  */
@@ -85,7 +90,7 @@ export type AppFieldExtendedReactFieldGroupApi<
85
90
  Subscribe: <TSelected = NoInfer<FieldGroupState<TFieldGroupData>>>(props: {
86
91
  selector?: (state: NoInfer<FieldGroupState<TFieldGroupData>>) => TSelected
87
92
  children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode
88
- }) => ReactNode
93
+ }) => ReturnType<FunctionComponent>
89
94
  }
90
95
 
91
96
  export function useFieldGroup<
package/src/useForm.tsx CHANGED
@@ -1,8 +1,8 @@
1
1
  'use client'
2
2
 
3
- import { FormApi, functionalUpdate } from '@tanstack/form-core'
3
+ import { FormApi, functionalUpdate, uuid } from '@tanstack/form-core'
4
4
  import { useStore } from '@tanstack/react-store'
5
- import { useId, useState } from 'react'
5
+ import { useMemo, useState } from 'react'
6
6
  import { Field } from './useField'
7
7
  import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
8
8
  import type {
@@ -13,7 +13,7 @@ import type {
13
13
  FormState,
14
14
  FormValidateOrFn,
15
15
  } from '@tanstack/form-core'
16
- import type { PropsWithChildren, ReactNode } from 'react'
16
+ import type { FunctionComponent, PropsWithChildren, ReactNode } from 'react'
17
17
  import type { FieldComponent } from './useField'
18
18
  import type { NoInfer } from '@tanstack/react-store'
19
19
 
@@ -89,7 +89,7 @@ export interface ReactFormApi<
89
89
  >,
90
90
  ) => TSelected
91
91
  children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode
92
- }) => ReactNode
92
+ }) => ReturnType<FunctionComponent>
93
93
  }
94
94
 
95
95
  /**
@@ -144,10 +144,10 @@ function LocalSubscribe({
144
144
  }: PropsWithChildren<{
145
145
  form: AnyFormApi
146
146
  selector: (state: AnyFormState) => AnyFormState
147
- }>) {
147
+ }>): ReturnType<FunctionComponent> {
148
148
  const data = useStore(form.store, selector)
149
149
 
150
- return functionalUpdate(children, data)
150
+ return <>{functionalUpdate(children, data)}</>
151
151
  }
152
152
 
153
153
  /**
@@ -184,10 +184,11 @@ export function useForm<
184
184
  TSubmitMeta
185
185
  >,
186
186
  ) {
187
- const formId = useId()
187
+ const fallbackFormId = useState(() => uuid())[0]
188
+ const [prevFormId, setPrevFormId] = useState<string>(opts?.formId as never)
188
189
 
189
- const [formApi] = useState(() => {
190
- const api = new FormApi<
190
+ const [formApi, setFormApi] = useState(() => {
191
+ return new FormApi<
191
192
  TFormData,
192
193
  TOnMount,
193
194
  TOnChange,
@@ -200,8 +201,16 @@ export function useForm<
200
201
  TOnDynamicAsync,
201
202
  TOnServer,
202
203
  TSubmitMeta
203
- >({ ...opts, formId: formId })
204
+ >({ ...opts, formId: opts?.formId ?? fallbackFormId })
205
+ })
206
+
207
+ if (prevFormId !== opts?.formId) {
208
+ const formId = opts?.formId ?? fallbackFormId
209
+ setFormApi(new FormApi({ ...opts, formId }))
210
+ setPrevFormId(formId)
211
+ }
204
212
 
213
+ const extendedFormApi = useMemo(() => {
205
214
  const extendedApi: ReactFormExtendedApi<
206
215
  TFormData,
207
216
  TOnMount,
@@ -215,16 +224,28 @@ export function useForm<
215
224
  TOnDynamicAsync,
216
225
  TOnServer,
217
226
  TSubmitMeta
218
- > = api as never
227
+ > = {
228
+ ...formApi,
229
+ handleSubmit: ((...props: never[]) => {
230
+ formApi._handleSubmit(...props)
231
+ }) as typeof formApi.handleSubmit,
232
+ // We must add all `get`ters from `core`'s `FormApi` here, as otherwise the spread operator won't catch those
233
+ get formId(): string {
234
+ return formApi._formId
235
+ },
236
+ get state() {
237
+ return formApi.store.state
238
+ },
239
+ } as never
219
240
 
220
241
  extendedApi.Field = function APIField(props) {
221
- return <Field {...props} form={api} />
242
+ return <Field {...props} form={formApi} />
222
243
  }
223
244
 
224
245
  extendedApi.Subscribe = function Subscribe(props: any) {
225
246
  return (
226
247
  <LocalSubscribe
227
- form={api}
248
+ form={formApi}
228
249
  selector={props.selector}
229
250
  children={props.children}
230
251
  />
@@ -232,7 +253,7 @@ export function useForm<
232
253
  }
233
254
 
234
255
  return extendedApi
235
- })
256
+ }, [formApi])
236
257
 
237
258
  useIsomorphicLayoutEffect(formApi.mount, [])
238
259
 
@@ -244,5 +265,5 @@ export function useForm<
244
265
  formApi.update(opts)
245
266
  })
246
267
 
247
- return formApi
268
+ return extendedFormApi
248
269
  }