Skip to content

Commit

Permalink
fix: React StrictMode should no longer crash
Browse files Browse the repository at this point in the history
* fix: react strict mode should no longer crash

Fixes #571

* docs: remove old apis from docs

* chore: return previous instance instead of generating a new one in React adapter

* chore: fix issue with `mount` running twice

* test: add test for this edgecase

* chore: migrate implementation of useIsomorphicEffectOnce

* chore: add name property to useForm

* Revert "chore: add name property to useForm"

This reverts commit a0b5ea7.

* chore: refactor internals to be more React-y

* docs: add a minor docs mention in our debugging guide

* chore: apply suggestion from fulopkovacs
  • Loading branch information
crutchcorn authored Mar 4, 2024
1 parent b0ab0b4 commit 9602113
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 133 deletions.
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

0 comments on commit 9602113

Please sign in to comment.