Skip to content

Remove UID and Field Instances #606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 4, 2024
Merged
5 changes: 4 additions & 1 deletion docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@
{
"label": "SSR/Next.js",
"to": "framework/react/guides/ssr"

},
{
"label": "Debugging",
"to": "framework/react/guides/debugging"
}
]
},
Expand Down
17 changes: 17 additions & 0 deletions docs/framework/react/guides/debugging.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 0 additions & 4 deletions docs/reference/fieldApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TParentData, TData>
```
Expand Down
43 changes: 20 additions & 23 deletions docs/reference/formApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ An object representing the options for a form.
defaultValues?: TData
```
- Set initial values for your form.

- ```tsx
defaultState?: Partial<FormState<TData>>
```
Expand Down Expand Up @@ -61,7 +61,7 @@ An object representing the options for a form.
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError
```
- Optional function that fires as soon as the component mounts.

- ```tsx
onChange?: (values: TData, formApi: FormApi<TData>) => ValidationError
```
Expand Down Expand Up @@ -102,17 +102,17 @@ A class representing the Form API. It handles the logic and interactions with th
options: FormOptions<TFormData> = {}
```
- The options for the form.

- ```tsx
store: Store<FormState<TFormData>>
```
- A [TanStack Store instance](https://tanstack.com/store/latest/docs/reference/Store) that keeps track of the form's state.

- ```tsx
state: FormState<TFormData>
```
- The current state of the form.

- ```tsx
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, TFormValidator>> =
{} as any
Expand Down Expand Up @@ -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<DeepKeys<TData>, 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
```
Expand All @@ -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<unknown, unknown> | undefined,
TFormValidator
>
>
> | null
```
- A record of field instances with unique identifiers as keys.
- An instance of the `FieldAPI`.

- ```tsx
validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>
Expand Down
17 changes: 1 addition & 16 deletions packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,6 @@ export type FieldMeta = {
isValidating: boolean
}

let uid = 0

export type FieldState<TData> = {
value: TData
meta: FieldMeta
Expand All @@ -259,7 +257,6 @@ export class FieldApi<
| undefined = undefined,
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
> {
uid: number
form: FieldApiOptions<
TParentData,
TName,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]
}
}
}

Expand Down
40 changes: 19 additions & 21 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,12 @@ export type FieldInfo<
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
> = {
instances: Record<
string,
FieldApi<
TFormData,
any,
Validator<unknown, unknown> | undefined,
TFormValidator
>
>
instance: FieldApi<
TFormData,
any,
Validator<unknown, unknown> | undefined,
TFormValidator
> | null
validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>
}

Expand Down Expand Up @@ -340,17 +337,17 @@ export class FormApi<
void (
Object.values(this.fieldInfo) as FieldInfo<any, TFormValidator>[]
).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 }))
}
})
})

Expand Down Expand Up @@ -587,7 +584,7 @@ export class FormApi<
): FieldInfo<TFormData, TFormValidator> => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return (this.fieldInfo[field] ||= {
instances: {},
instance: null,
validationMetaMap: {
onChange: undefined,
onBlur: undefined,
Expand Down Expand Up @@ -645,6 +642,7 @@ export class FormApi<

return newState
})
delete this.fieldInfo[field]
}

pushFieldValue = <TField extends DeepKeys<TFormData>>(
Expand Down
6 changes: 3 additions & 3 deletions packages/form-core/src/tests/FieldApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,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()
})

Expand All @@ -624,8 +624,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()
Expand Down
Loading