diff --git a/docs/config.json b/docs/config.json index ea14d4fd6..eaf6aec8b 100644 --- a/docs/config.json +++ b/docs/config.json @@ -85,7 +85,10 @@ { "label": "SSR/Next.js", "to": "framework/react/guides/ssr" - + }, + { + "label": "Debugging", + "to": "framework/react/guides/debugging" } ] }, diff --git a/docs/framework/react/guides/debugging.md b/docs/framework/react/guides/debugging.md new file mode 100644 index 000000000..305b2f35e --- /dev/null +++ b/docs/framework/react/guides/debugging.md @@ -0,0 +1,17 @@ +--- +id: debugging +title: Debugging React Usage +--- + +Here's a list of common errors you might see in the console and how to fix them. + +# Changing an uncontrolled input to be controlled + +If you see this error in the console: + +``` +Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components +``` + +It's likely you forgot the `defaultValues` in your `useForm` Hook or `form.Field` component usage. This is occurring +because the input is being rendered before the form value is initialized and is therefore changing from `undefined` to `""` when a text input is made. diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md index 14737e651..5b1006df4 100644 --- a/docs/framework/react/guides/ssr.md +++ b/docs/framework/react/guides/ssr.md @@ -76,7 +76,7 @@ export default async function someAction(prev: unknown, formData: FormData) { return await formFactory.validateFormData(formData); } ``` -- The action we've discussed is straightforward yet essential. It activates exclusively on the server side when we submit our form. In the example given, the action employs `formFactory.validateFormData(formData)`. This function takes care of validating the data received from the client during form submission. It's ab efficient way to ensure data compliance with our predefined server rules. +- The action we've discussed is straightforward yet essential. It activates exclusively on the server side when we submit our form. In the example given, the action employs `formFactory.validateFormData(formData)`. This function takes care of validating the data received from the client during form submission. It's an efficient way to ensure data compliance with our predefined server rules. - Now, let's shift our focus to the client component: - For those who might be exploring this for the first time, `useFormState` is a relatively new hook in React. You can find more details in React's documentation on [useFormState](https://react.dev/reference/react-dom/hooks/useFormState). This hook represents a significant advancement as it allows us to dynamically update the state based on the outcomes of a form action. It's an effective way to manage form states, especially in response to server-side interactions. diff --git a/docs/framework/react/reference/formApi.md b/docs/framework/react/reference/formApi.md index e1d93401d..1a966802c 100644 --- a/docs/framework/react/reference/formApi.md +++ b/docs/framework/react/reference/formApi.md @@ -32,3 +32,4 @@ When using `@tanstack/react-form`, the [core form API](../../reference/formApi) }) => JSX.Element ``` - 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. + > Note that TypeScript `5.0.4` and older versions will incorrectly complain if the `selector` method doesn't return the form's full state (`state`). This is caused by a [bug in TypeScript](https://github.com/TanStack/form/pull/606#discussion_r1506715714), and you can safely ignore it with `//@ts-expect-error` directive. diff --git a/docs/reference/fieldApi.md b/docs/reference/fieldApi.md index 3f337c5ea..01daa1d0a 100644 --- a/docs/reference/fieldApi.md +++ b/docs/reference/fieldApi.md @@ -164,10 +164,6 @@ A class representing the API for managing a form field. #### Properties -- ```tsx - uid: number - ``` - - A unique identifier for the field instance. - ```tsx form: FormApi ``` diff --git a/docs/reference/formApi.md b/docs/reference/formApi.md index 3b71c867c..e79ed610a 100644 --- a/docs/reference/formApi.md +++ b/docs/reference/formApi.md @@ -29,7 +29,7 @@ An object representing the options for a form. defaultValues?: TData ``` - Set initial values for your form. - + - ```tsx defaultState?: Partial> ``` @@ -61,7 +61,7 @@ An object representing the options for a form. onMount?: (values: TData, formApi: FormApi) => ValidationError ``` - Optional function that fires as soon as the component mounts. - + - ```tsx onChange?: (values: TData, formApi: FormApi) => ValidationError ``` @@ -102,17 +102,17 @@ A class representing the Form API. It handles the logic and interactions with th options: FormOptions = {} ``` - The options for the form. - + - ```tsx store: Store> ``` - A [TanStack Store instance](https://tanstack.com/store/latest/docs/reference/Store) that keeps track of the form's state. - + - ```tsx state: FormState ``` - The current state of the form. - + - ```tsx fieldInfo: Record, FieldInfo> = {} as any @@ -197,62 +197,62 @@ An object representing the current state of the form. errorMap: ValidationErrorMap ``` - The error map for the form itself. - + - ```tsx isFormValidating: boolean ``` - A boolean indicating if the form is currently validating. - + - ```tsx isFormValid: boolean ``` - A boolean indicating if the form is valid. - + - ```tsx fieldMeta: Record, FieldMeta> ``` - A record of field metadata for each field in the form. - + - ```tsx isFieldsValidating: boolean ``` - A boolean indicating if any of the form fields are currently validating. - + - ```tsx isFieldsValid: boolean ``` - A boolean indicating if all the form fields are valid. - + - ```tsx isSubmitting: boolean ``` - A boolean indicating if the form is currently submitting. - + - ```tsx isTouched: boolean ``` - A boolean indicating if any of the form fields have been touched. - + - ```tsx isSubmitted: boolean ``` - A boolean indicating if the form has been submitted. - + - ```tsx isValidating: boolean ``` - A boolean indicating if the form or any of its fields are currently validating. - + - ```tsx isValid: boolean ``` - A boolean indicating if the form and all its fields are valid. - + - ```tsx canSubmit: boolean ``` - A boolean indicating if the form can be submitted based on its current state. - + - ```tsx submissionAttempts: number ``` @@ -268,17 +268,14 @@ An object representing the current state of the form. An object representing the field information for a specific field within the form. - ```tsx - instances: Record< - string, - FieldApi< + instance: FieldApi< TFormData, any, Validator | undefined, TFormValidator - > - > + > | null ``` - - A record of field instances with unique identifiers as keys. + - An instance of the `FieldAPI`. - ```tsx validationMetaMap: Record diff --git a/examples/react/next-server-actions/package.json b/examples/react/next-server-actions/package.json index 656225311..16cbd86f8 100644 --- a/examples/react/next-server-actions/package.json +++ b/examples/react/next-server-actions/package.json @@ -12,7 +12,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "next": "14.0.4", - "@tanstack/react-form": "^0.13.6" + "@tanstack/react-form": "^0.13.7" }, "devDependencies": { "typescript": "5.2.2", diff --git a/examples/react/simple/package.json b/examples/react/simple/package.json index 4f4f32ea4..504a077f0 100644 --- a/examples/react/simple/package.json +++ b/examples/react/simple/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-form": "^0.13.6", + "@tanstack/react-form": "^0.13.7", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react/ui-libraries/package.json b/examples/react/ui-libraries/package.json index 8f0a7421f..996a48615 100644 --- a/examples/react/ui-libraries/package.json +++ b/examples/react/ui-libraries/package.json @@ -14,7 +14,7 @@ "@mantine/core": "7.3.2", "@mantine/hooks": "7.3.2", "@mui/material": "5.15.2", - "@tanstack/react-form": "^0.13.6", + "@tanstack/react-form": "^0.13.7", "@yme/lay-postcss": "0.1.0", "postcss": "8.4.32", "postcss-preset-mantine": "1.12.2", diff --git a/examples/react/valibot/package.json b/examples/react/valibot/package.json index 0006022aa..d6082f412 100644 --- a/examples/react/valibot/package.json +++ b/examples/react/valibot/package.json @@ -9,8 +9,8 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-form": "^0.13.6", - "@tanstack/valibot-form-adapter": "^0.13.6", + "@tanstack/react-form": "^0.13.7", + "@tanstack/valibot-form-adapter": "^0.13.7", "react": "^18.2.0", "react-dom": "^18.2.0", "valibot": "^0.20.1" diff --git a/examples/react/yup/package.json b/examples/react/yup/package.json index 9b8cd8566..294ff0408 100644 --- a/examples/react/yup/package.json +++ b/examples/react/yup/package.json @@ -9,8 +9,8 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-form": "^0.13.6", - "@tanstack/yup-form-adapter": "^0.13.6", + "@tanstack/react-form": "^0.13.7", + "@tanstack/yup-form-adapter": "^0.13.7", "react": "^18.2.0", "react-dom": "^18.2.0", "yup": "^1.3.2" diff --git a/examples/react/zod/package.json b/examples/react/zod/package.json index cf1efa580..92ea4de96 100644 --- a/examples/react/zod/package.json +++ b/examples/react/zod/package.json @@ -9,8 +9,8 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-form": "^0.13.6", - "@tanstack/zod-form-adapter": "^0.13.6", + "@tanstack/react-form": "^0.13.7", + "@tanstack/zod-form-adapter": "^0.13.7", "react": "^18.2.0", "react-dom": "^18.2.0", "zod": "^3.22.4" diff --git a/examples/solid/simple/package.json b/examples/solid/simple/package.json index 2a167a32b..cbcbad862 100644 --- a/examples/solid/simple/package.json +++ b/examples/solid/simple/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/solid-form": "^0.13.6", + "@tanstack/solid-form": "^0.13.7", "solid-js": "^1.7.8" }, "devDependencies": { diff --git a/examples/solid/valibot/package.json b/examples/solid/valibot/package.json index be812c34d..561a463cf 100644 --- a/examples/solid/valibot/package.json +++ b/examples/solid/valibot/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/solid-form": "^0.13.6", - "@tanstack/valibot-form-adapter": "^0.13.6", + "@tanstack/solid-form": "^0.13.7", + "@tanstack/valibot-form-adapter": "^0.13.7", "solid-js": "^1.7.8", "valibot": "^0.20.1" }, diff --git a/examples/solid/yup/package.json b/examples/solid/yup/package.json index 956300462..014a12079 100644 --- a/examples/solid/yup/package.json +++ b/examples/solid/yup/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/solid-form": "^0.13.6", - "@tanstack/yup-form-adapter": "^0.13.6", + "@tanstack/solid-form": "^0.13.7", + "@tanstack/yup-form-adapter": "^0.13.7", "solid-js": "^1.7.8", "yup": "^1.3.2" }, diff --git a/examples/solid/zod/package.json b/examples/solid/zod/package.json index e72e8fa8e..889956f25 100644 --- a/examples/solid/zod/package.json +++ b/examples/solid/zod/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/solid-form": "^0.13.6", - "@tanstack/zod-form-adapter": "^0.13.6", + "@tanstack/solid-form": "^0.13.7", + "@tanstack/zod-form-adapter": "^0.13.7", "solid-js": "^1.7.8", "zod": "^3.22.4" }, diff --git a/examples/vue/simple/package.json b/examples/vue/simple/package.json index 68f783b46..2e0fb2337 100644 --- a/examples/vue/simple/package.json +++ b/examples/vue/simple/package.json @@ -9,7 +9,7 @@ "serve": "vite preview" }, "dependencies": { - "@tanstack/vue-form": "^0.13.6", + "@tanstack/vue-form": "^0.13.7", "vue": "^3.3.4" }, "devDependencies": { diff --git a/examples/vue/valibot/package.json b/examples/vue/valibot/package.json index 9e7778e62..389bbe4e6 100644 --- a/examples/vue/valibot/package.json +++ b/examples/vue/valibot/package.json @@ -9,8 +9,8 @@ "serve": "vite preview" }, "dependencies": { - "@tanstack/vue-form": "^0.13.6", - "@tanstack/valibot-form-adapter": "^0.13.6", + "@tanstack/vue-form": "^0.13.7", + "@tanstack/valibot-form-adapter": "^0.13.7", "vue": "^3.3.4", "valibot": "^0.20.1" }, diff --git a/examples/vue/yup/package.json b/examples/vue/yup/package.json index 51afed7e4..73e4e99d4 100644 --- a/examples/vue/yup/package.json +++ b/examples/vue/yup/package.json @@ -9,8 +9,8 @@ "serve": "vite preview" }, "dependencies": { - "@tanstack/vue-form": "^0.13.6", - "@tanstack/yup-form-adapter": "^0.13.6", + "@tanstack/vue-form": "^0.13.7", + "@tanstack/yup-form-adapter": "^0.13.7", "vue": "^3.3.4", "yup": "^1.3.2" }, diff --git a/examples/vue/zod/package.json b/examples/vue/zod/package.json index 4edefc3f7..805efa4ab 100644 --- a/examples/vue/zod/package.json +++ b/examples/vue/zod/package.json @@ -9,8 +9,8 @@ "serve": "vite preview" }, "dependencies": { - "@tanstack/vue-form": "^0.13.6", - "@tanstack/zod-form-adapter": "^0.13.6", + "@tanstack/vue-form": "^0.13.7", + "@tanstack/zod-form-adapter": "^0.13.7", "vue": "^3.3.4", "zod": "^3.22.4" }, diff --git a/packages/form-core/package.json b/packages/form-core/package.json index 9907401cc..e9eb96847 100644 --- a/packages/form-core/package.json +++ b/packages/form-core/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/form-core", - "version": "0.13.6", + "version": "0.13.7", "description": "Powerful, type-safe, framework agnostic forms.", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 2035b8ca9..14dab90d6 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -237,8 +237,6 @@ export type FieldMeta = { isValidating: boolean } -let uid = 0 - export type FieldState = { value: TData meta: FieldMeta @@ -259,7 +257,6 @@ export class FieldApi< | undefined = undefined, TData extends DeepValue = DeepValue, > { - uid: number form: FieldApiOptions< TParentData, TName, @@ -289,13 +286,6 @@ export class FieldApi< >, ) { this.form = opts.form as never - this.uid = uid++ - // Support field prefixing from FieldScope - // let fieldPrefix = '' - // if (this.form.fieldName) { - // fieldPrefix = `${this.form.fieldName}.` - // } - this.name = opts.name as never if (opts.defaultValue !== undefined) { @@ -362,7 +352,7 @@ export class FieldApi< mount = () => { const info = this.getInfo() - info.instances[this.uid] = this as never + info.instance = this as never const unsubscribe = this.form.store.subscribe(() => { this.store.batch(() => { const nextValue = this.getValue() @@ -403,13 +393,8 @@ export class FieldApi< const preserveValue = this.options.preserveValue unsubscribe() if (!preserveValue) { - delete info.instances[this.uid] this.form.deleteField(this.name) } - - if (!Object.keys(info.instances).length && !preserveValue) { - delete this.form.fieldInfo[this.name] - } } } diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index ae5cfab4b..abc3f8bce 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -104,15 +104,12 @@ export type FieldInfo< TFormData, TFormValidator extends Validator | undefined = undefined, > = { - instances: Record< - string, - FieldApi< - TFormData, - any, - Validator | undefined, - TFormValidator - > - > + instance: FieldApi< + TFormData, + any, + Validator | undefined, + TFormValidator + > | null validationMetaMap: Record } @@ -340,17 +337,17 @@ export class FormApi< void ( Object.values(this.fieldInfo) as FieldInfo[] ).forEach((field) => { - Object.values(field.instances).forEach((instance) => { - // Validate the field - fieldValidationPromises.push( - Promise.resolve().then(() => instance.validate(cause)), - ) - // If any fields are not touched - if (!instance.state.meta.isTouched) { - // Mark them as touched - instance.setMeta((prev) => ({ ...prev, isTouched: true })) - } - }) + if (!field.instance) return + const fieldInstance = field.instance + // Validate the field + fieldValidationPromises.push( + Promise.resolve().then(() => fieldInstance.validate(cause)), + ) + // If any fields are not touched + if (!field.instance.state.meta.isTouched) { + // Mark them as touched + field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) + } }) }) @@ -587,7 +584,7 @@ export class FormApi< ): FieldInfo => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return (this.fieldInfo[field] ||= { - instances: {}, + instance: null, validationMetaMap: { onChange: undefined, onBlur: undefined, @@ -645,6 +642,7 @@ export class FormApi< return newState }) + delete this.fieldInfo[field] } pushFieldValue = >( diff --git a/packages/form-core/src/tests/FieldApi.spec.ts b/packages/form-core/src/tests/FieldApi.spec.ts index a0b31a260..4bdbe13e3 100644 --- a/packages/form-core/src/tests/FieldApi.spec.ts +++ b/packages/form-core/src/tests/FieldApi.spec.ts @@ -582,7 +582,7 @@ describe('field api', () => { const unmount = field.mount() unmount() - expect(form.getFieldInfo(field.name).instances[field.uid]).toBeDefined() + expect(form.getFieldInfo(field.name).instance).toBeDefined() expect(form.getFieldInfo(field.name)).toBeDefined() }) @@ -604,8 +604,8 @@ describe('field api', () => { unmount() const info = form.getFieldInfo(field.name) subscription() - expect(info.instances[field.uid]).toBeUndefined() - expect(Object.keys(info.instances).length).toBe(0) + expect(info.instance).toBeNull() + expect(Object.keys(info.instance ?? {}).length).toBe(0) // Check that form store has been updated expect(callback).toHaveBeenCalledOnce() diff --git a/packages/react-form/package.json b/packages/react-form/package.json index 51037d277..c79bd3544 100644 --- a/packages/react-form/package.json +++ b/packages/react-form/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-form", - "version": "0.13.6", + "version": "0.13.7", "description": "Powerful, type-safe forms for React.", "author": "tannerlinsley", "license": "MIT", @@ -15,7 +15,7 @@ "clean": "rimraf ./dist && rimraf ./coverage", "test:eslint": "eslint --ext .ts,.tsx ./src", "test:types:versions49": "node ../../node_modules/typescript49/lib/tsc.js --project tsconfig.legacy.json", - "test:types:versions50": "node ../../node_modules/typescript50/lib/tsc.js", + "test:types:versions50": "node ../../node_modules/typescript50/lib/tsc.js --project tsconfig.50.json", "test:types:versions51": "node ../../node_modules/typescript51/lib/tsc.js", "test:types:versions52": "tsc", "test:types": "pnpm run \"/^test:types:versions.*/\"", diff --git a/packages/react-form/src/tests/useField.test.tsx b/packages/react-form/src/tests/useField.test.tsx index a8e4e9f69..24b805c03 100644 --- a/packages/react-form/src/tests/useField.test.tsx +++ b/packages/react-form/src/tests/useField.test.tsx @@ -3,9 +3,9 @@ import * as React from 'react' import { render, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import '@testing-library/jest-dom' -import { createFormFactory } from '../index' +import { createFormFactory, useForm } from '../index' import { sleep } from './utils' -import type { FormApi } from '../index' +import type { FieldApi, FormApi } from '../index' const user = userEvent.setup() @@ -101,7 +101,12 @@ describe('useField', () => { const formFactory = createFormFactory() function Comp() { - const form = formFactory.useForm() + const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) return ( @@ -143,7 +148,12 @@ describe('useField', () => { const formFactory = createFormFactory() function Comp() { - const form = formFactory.useForm() + const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) return ( @@ -188,7 +198,12 @@ describe('useField', () => { const formFactory = createFormFactory() function Comp() { - const form = formFactory.useForm() + const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) return ( @@ -239,7 +254,12 @@ describe('useField', () => { const formFactory = createFormFactory() function Comp() { - const form = formFactory.useForm() + const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) return ( @@ -288,7 +308,12 @@ describe('useField', () => { const formFactory = createFormFactory() function Comp() { - const form = formFactory.useForm() + const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) return ( @@ -346,7 +371,12 @@ describe('useField', () => { const formFactory = createFormFactory() function Comp() { - const form = formFactory.useForm() + const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) return ( @@ -395,7 +425,12 @@ describe('useField', () => { const formFactory = createFormFactory() let form: FormApi | null = null function Comp() { - form = formFactory.useForm() + form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) return ( { const formFactory = createFormFactory() let form: FormApi | null = null function Comp() { - form = formFactory.useForm() + form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) return ( { const info = form!.fieldInfo expect(Object.keys(info)).toHaveLength(0) }) + + it('should handle strict mode properly with conditional fields', async () => { + function FieldInfo({ field }: { field: FieldApi }) { + return ( + <> + {field.state.meta.touchedErrors ? ( + {field.state.meta.touchedErrors} + ) : null} + {field.state.meta.isValidating ? 'Validating...' : null} + + ) + } + + function Comp() { + const [showField, setShowField] = React.useState(true) + + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + onSubmit: async () => {}, + }) + + return ( +
+ +
{ + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + {showField ? ( + + !value ? 'A first name is required' : undefined, + }} + children={(field) => { + // Avoid hasty abstractions. Render props are great! + return ( + <> + + field.handleChange(e.target.value)} + /> + + + ) + }} + /> + ) : null} +
+
+ ( + <> + + field.handleChange(e.target.value)} + /> + + + )} + /> +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> + + +
+
+ ) + } + + const { getByText, findByText, queryByText } = render( + + + , + ) + + await user.click(getByText('Submit')) + expect(await findByText('A first name is required')).toBeInTheDocument() + await user.click(getByText('Hide field')) + await user.click(getByText('Submit')) + expect(queryByText('A first name is required')).not.toBeInTheDocument() + }) }) diff --git a/packages/react-form/src/tests/useForm.test.tsx b/packages/react-form/src/tests/useForm.test.tsx index 34d1d4f23..fc5b67629 100644 --- a/packages/react-form/src/tests/useForm.test.tsx +++ b/packages/react-form/src/tests/useForm.test.tsx @@ -172,6 +172,10 @@ describe('useForm', () => { function Comp() { const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, validators: { onChange() { return error @@ -217,6 +221,10 @@ describe('useForm', () => { function Comp() { const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, validators: { onChange: ({ value }) => value.firstName === 'other' ? error : undefined, @@ -262,6 +270,10 @@ describe('useForm', () => { function Comp() { const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, validators: { onChange: ({ value }) => value.firstName === 'other' ? error : undefined, @@ -362,6 +374,10 @@ describe('useForm', () => { function Comp() { const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, validators: { onChangeAsync: async () => { await sleep(10) @@ -412,6 +428,10 @@ describe('useForm', () => { function Comp() { const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, validators: { onChangeAsync: async () => { await sleep(10) @@ -472,6 +492,10 @@ describe('useForm', () => { function Comp() { const form = formFactory.useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, validators: { onChangeAsyncDebounceMs: 100, onChangeAsync: async () => { diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index 299d6c06b..afebf4a0d 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -1,9 +1,8 @@ -import React, { useRef, useState } from 'rehackt' +import React, { useState } from 'rehackt' import { useStore } from '@tanstack/react-store' import { FieldApi, functionalUpdate } from '@tanstack/form-core' import { formContext, useFormContext } from './formContext' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' -import { useIsomorphicEffectOnce } from './useIsomorphicEffectOnce' import type { UseFieldOptions } from './types' import type { DeepKeys, @@ -88,6 +87,8 @@ export function useField< return api }) + useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi]) + /** * fieldApi.update should not have any side effects. Think of it like a `useRef` * that we need to keep updated every render with the most up-to-date information. @@ -104,19 +105,6 @@ export function useField< } : undefined, ) - const unmountFn = useRef<(() => void) | null>(null) - - useIsomorphicEffectOnce(() => { - return () => { - unmountFn.current?.() - } - }) - - // We have to mount it right as soon as it renders, otherwise we get: - // https://github.com/TanStack/form/issues/523 - if (!unmountFn.current) { - unmountFn.current = fieldApi.mount() - } return fieldApi as never } diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index e69d72db2..61745a1cc 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -22,6 +22,17 @@ declare module '@tanstack/form-core' { selector?: (state: NoInfer>) => TSelected, ) => TSelected Subscribe: >>(props: { + /** + TypeScript versions <=5.0.4 have a bug that prevents + the type of the `TSelected` generic from being inferred + from the return type of this method. + + In these versions, `TSelected` will fall back to the default + type (or `unknown` if that's not defined). + + @see {@link https://github.com/TanStack/form/pull/606/files#r1506715714 | This discussion on GitHub for the details} + @see {@link https://github.com/microsoft/TypeScript/issues/52786 | The bug report in `microsoft/TypeScript`} + */ selector?: (state: NoInfer>) => TSelected children: ((state: NoInfer) => ReactNode) | ReactNode }) => JSX.Element @@ -35,7 +46,6 @@ export function useForm< opts?: FormOptions, ): FormApi { const [formApi] = useState(() => { - // @ts-ignore const api = new FormApi(opts) api.Provider = function Provider(props) { diff --git a/packages/react-form/src/useIsomorphicEffectOnce.ts b/packages/react-form/src/useIsomorphicEffectOnce.ts deleted file mode 100644 index ee0fc972a..000000000 --- a/packages/react-form/src/useIsomorphicEffectOnce.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useRef, useState } from 'rehackt' -import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' -import type { EffectCallback } from 'rehackt' - -/** - * This hook handles StrictMode and prod mode - */ -export const useIsomorphicEffectOnce = (effect: EffectCallback) => { - const destroyFunc = useRef void)>() - const effectCalled = useRef(false) - const renderAfterCalled = useRef(false) - const [val, setVal] = useState(0) - - if (effectCalled.current) { - renderAfterCalled.current = true - } - - useIsomorphicLayoutEffect(() => { - // only execute the effect first time around - if (!effectCalled.current) { - destroyFunc.current = effect() - effectCalled.current = true - } - - // this forces one render after the effect is run - setVal((v) => v + 1) - - return () => { - // if the comp didn't render since the useEffect was called, - // we know it's the dummy React cycle - if (!renderAfterCalled.current) { - return - } - if (destroyFunc.current) { - destroyFunc.current() - } - } - }, []) -} diff --git a/packages/react-form/tsconfig.50.json b/packages/react-form/tsconfig.50.json new file mode 100644 index 000000000..7eb52de1e --- /dev/null +++ b/packages/react-form/tsconfig.50.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/tests/**"] +} diff --git a/packages/solid-form/package.json b/packages/solid-form/package.json index acf23cc51..c029df2ca 100644 --- a/packages/solid-form/package.json +++ b/packages/solid-form/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/solid-form", - "version": "0.13.6", + "version": "0.13.7", "description": "Powerful, type-safe forms for Solid.", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/valibot-form-adapter/package.json b/packages/valibot-form-adapter/package.json index 3ec3c6daa..15e0acdbd 100644 --- a/packages/valibot-form-adapter/package.json +++ b/packages/valibot-form-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/valibot-form-adapter", - "version": "0.13.6", + "version": "0.13.7", "description": "The Valibot adapter for TanStack Form.", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/vue-form/package.json b/packages/vue-form/package.json index 1116ffea2..4e7a7ccde 100644 --- a/packages/vue-form/package.json +++ b/packages/vue-form/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/vue-form", - "version": "0.13.6", + "version": "0.13.7", "description": "Powerful, type-safe forms for Vue.", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/yup-form-adapter/package.json b/packages/yup-form-adapter/package.json index 54d682c94..2541fdea3 100644 --- a/packages/yup-form-adapter/package.json +++ b/packages/yup-form-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/yup-form-adapter", - "version": "0.13.6", + "version": "0.13.7", "description": "The Yup adapter for TanStack Form.", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/zod-form-adapter/package.json b/packages/zod-form-adapter/package.json index d7a05f1eb..2a0f91ce1 100644 --- a/packages/zod-form-adapter/package.json +++ b/packages/zod-form-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/zod-form-adapter", - "version": "0.13.6", + "version": "0.13.7", "description": "The Zod adapter for TanStack Form.", "author": "tannerlinsley", "license": "MIT", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8da403ba3..da65159a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,7 +123,7 @@ importers: examples/react/next-server-actions: dependencies: '@tanstack/react-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/react-form next: specifier: 14.0.4 @@ -157,7 +157,7 @@ importers: examples/react/simple: dependencies: '@tanstack/react-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/react-form react: specifier: ^18.2.0 @@ -191,7 +191,7 @@ importers: specifier: 5.15.2 version: 5.15.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) '@tanstack/react-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/react-form '@yme/lay-postcss': specifier: 0.1.0 @@ -252,10 +252,10 @@ importers: examples/react/valibot: dependencies: '@tanstack/react-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/react-form '@tanstack/valibot-form-adapter': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/valibot-form-adapter react: specifier: ^18.2.0 @@ -277,10 +277,10 @@ importers: examples/react/yup: dependencies: '@tanstack/react-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/react-form '@tanstack/yup-form-adapter': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/yup-form-adapter react: specifier: ^18.2.0 @@ -302,10 +302,10 @@ importers: examples/react/zod: dependencies: '@tanstack/react-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/react-form '@tanstack/zod-form-adapter': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/zod-form-adapter react: specifier: ^18.2.0 @@ -327,7 +327,7 @@ importers: examples/solid/simple: dependencies: '@tanstack/solid-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/solid-form solid-js: specifier: ^1.7.8 @@ -346,10 +346,10 @@ importers: examples/solid/valibot: dependencies: '@tanstack/solid-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/solid-form '@tanstack/valibot-form-adapter': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/valibot-form-adapter solid-js: specifier: ^1.7.8 @@ -371,10 +371,10 @@ importers: examples/solid/yup: dependencies: '@tanstack/solid-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/solid-form '@tanstack/yup-form-adapter': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/yup-form-adapter solid-js: specifier: ^1.7.8 @@ -396,10 +396,10 @@ importers: examples/solid/zod: dependencies: '@tanstack/solid-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/solid-form '@tanstack/zod-form-adapter': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/zod-form-adapter solid-js: specifier: ^1.7.8 @@ -421,7 +421,7 @@ importers: examples/vue/simple: dependencies: '@tanstack/vue-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/vue-form vue: specifier: ^3.3.4 @@ -443,10 +443,10 @@ importers: examples/vue/valibot: dependencies: '@tanstack/valibot-form-adapter': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/valibot-form-adapter '@tanstack/vue-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/vue-form valibot: specifier: ^0.20.1 @@ -471,10 +471,10 @@ importers: examples/vue/yup: dependencies: '@tanstack/vue-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/vue-form '@tanstack/yup-form-adapter': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/yup-form-adapter vue: specifier: ^3.3.4 @@ -499,10 +499,10 @@ importers: examples/vue/zod: dependencies: '@tanstack/vue-form': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/vue-form '@tanstack/zod-form-adapter': - specifier: ^0.13.6 + specifier: ^0.13.7 version: link:../../../packages/zod-form-adapter vue: specifier: ^3.3.4