Skip to content

Commit

Permalink
Merge pull request #38 from stevent-team/fix/rerenders-and-nullable-c…
Browse files Browse the repository at this point in the history
…ontrolled

Fix unnecessary rerenders and nullable controlled values
  • Loading branch information
GRA0007 authored Aug 10, 2023
2 parents acb4977 + b9a77a9 commit 384a1b0
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/modern-sloths-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stevent-team/react-zoom-form": patch
---

Store formValues in a ref to prevent unnecessary rerenders
5 changes: 5 additions & 0 deletions .changeset/smart-kangaroos-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stevent-team/react-zoom-form": patch
---

Fix an issue where controlled fields with a nullable value would break the types
14 changes: 7 additions & 7 deletions lib/field.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { z } from 'zod'
import { PartialObject, PathSegment, RecursivePartial, arrayStartsWith, getDeepProp, isRadio, setDeepProp, unwrapZodType } from './utils'
import { NonUndefined, PartialObject, PathSegment, RecursivePartial, arrayStartsWith, getDeepProp, isRadio, setDeepProp, unwrapZodType } from './utils'
import { FieldRefs } from './useForm'

/** The controls that each path along the field chain can access under `_field`. */
export type Field<Schema extends z.ZodType = z.ZodType> = {
_field: {
schema: Schema
path: PathSegment[]
formValue: RecursivePartial<z.ZodType>
formValue: React.MutableRefObject<RecursivePartial<z.TypeOf<Schema>>>
setFormValue: React.Dispatch<React.SetStateAction<RecursivePartial<z.ZodType>>>
formErrors: z.ZodError<z.ZodType> | undefined
}
Expand Down Expand Up @@ -112,13 +112,13 @@ export const register: RegisterFn = (path, fieldSchema, setFormValue, fieldRefs,
* <CustomField {...controlled(fields.myCustomField)} />
* ```
*/
export const controlled = <T>({ _field }: Field<z.ZodType<NonNullable<T>>>): ControlledField<NonNullable<T>> => {
export const controlled = <T>({ _field }: Field<z.ZodType<NonUndefined<T>>>): ControlledField<NonUndefined<T>> => {
const { schema, path, formValue, setFormValue, formErrors } = _field

return {
schema,
name: path.map(p => p.key).join('.'),
value: getDeepProp(formValue, path) as PartialObject<NonNullable<T>> | undefined,
value: getDeepProp(formValue.current, path) as PartialObject<NonUndefined<T>> | undefined,
onChange: value => setFormValue(v => setDeepProp(v, path, value) as typeof v),
errors: formErrors?.issues?.filter(issue => arrayStartsWith(issue.path, path.map(p => p.key))) ?? [],
}
Expand Down Expand Up @@ -148,7 +148,7 @@ export const fieldErrors = <T>({ _field: { formErrors, path } }: Field<z.ZodType
* ```
*/
export const getValue = <T>({ _field: { formValue, path } }: Field<z.ZodType<T>>) =>
getDeepProp(formValue, path) as PartialObject<NonNullable<T>> | undefined
getDeepProp(formValue.current, path) as PartialObject<NonUndefined<T>> | undefined

/**
* Set the value of a field directly.
Expand All @@ -161,10 +161,10 @@ export const getValue = <T>({ _field: { formValue, path } }: Field<z.ZodType<T>>
*/
export const setValue = <T>(
{ _field: { setFormValue, path } }: Field<z.ZodType<T>>,
newValue: PartialObject<NonNullable<T>> | undefined | ((currentValue: PartialObject<NonNullable<T>> | undefined) => PartialObject<NonNullable<T>> | undefined)
newValue: PartialObject<NonUndefined<T>> | undefined | ((currentValue: PartialObject<NonUndefined<T>> | undefined) => PartialObject<NonUndefined<T>> | undefined)
) => {
if (typeof newValue === 'function') {
setFormValue(v => setDeepProp(v, path, newValue(getDeepProp(v, path) as PartialObject<NonNullable<T>> | undefined)) as typeof v)
setFormValue(v => setDeepProp(v, path, newValue(getDeepProp(v, path) as PartialObject<NonUndefined<T>> | undefined)) as typeof v)
} else {
setFormValue(v => setDeepProp(v, path, newValue) as typeof v)
}
Expand Down
27 changes: 20 additions & 7 deletions lib/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,20 @@ export const useForm = <Schema extends z.ZodTypeAny>({
schema,
initialValues = {},
}: UseFormOptions<Schema>) => {
const [formValue, setFormValue] = useState(structuredClone(initialValues))
const [formValueState, _setFormValue] = useState(structuredClone(initialValues))
const formValue = useRef(structuredClone(initialValues))

// Set the form value state and ref
const setFormValue = useCallback<typeof _setFormValue>(value => {
if (typeof value === 'function') {
_setFormValue(value(formValue.current))
formValue.current = value(formValue.current)
} else {
_setFormValue(value)
formValue.current = value
}
}, [])

const [formErrors, setFormErrors] = useState<z.ZodError<z.infer<Schema>>>()
const fieldRefs = useRef<FieldRefs>({})

Expand All @@ -54,7 +67,7 @@ export const useForm = <Schema extends z.ZodTypeAny>({

// Keep track of the initial form values to calculate isDirty
const [internalInitialValues, setInternalInitialValues] = useState(structuredClone(initialValues))
const isDirty = useMemo(() => !deepEqual(formValue, internalInitialValues), [formValue, internalInitialValues])
const isDirty = useMemo(() => !deepEqual(formValueState, internalInitialValues), [formValueState, internalInitialValues])

const reset = useCallback<UseFormReturn<Schema>['reset']>((values = initialValues) => {
setValidateOnChange(false)
Expand All @@ -64,22 +77,22 @@ export const useForm = <Schema extends z.ZodTypeAny>({

// Validate by parsing form data with zod schema, and return parsed data if valid
const validate = useCallback(async () => {
const parsed = await schema.safeParseAsync(formValue)
const parsed = await schema.safeParseAsync(formValue.current)
if (parsed.success) {
setFormErrors(undefined)
return parsed.data
} else {
setFormErrors(parsed.error)
}
}, [schema, formValue])
}, [schema])

// Watch for changes in value
useEffect(() => {
if (validateOnChange) validate()

// Set registered field values
Object.values(fieldRefs.current).forEach(({ path, ref }) => {
const value = getDeepProp(formValue, path) as string | boolean | undefined
const value = getDeepProp(formValueState, path) as string | boolean | undefined
if (isRadio(ref)) {
if (ref.value === value) {
ref.checked = true
Expand All @@ -92,7 +105,7 @@ export const useForm = <Schema extends z.ZodTypeAny>({
ref.value = String(value ?? '')
}
})
}, [formValue, validateOnChange, validate])
}, [formValueState, validateOnChange, validate])

// Submit handler
const handleSubmit = useCallback<UseFormReturn<Schema>['handleSubmit']>(handler => async e => {
Expand All @@ -105,7 +118,7 @@ export const useForm = <Schema extends z.ZodTypeAny>({

const fields = useMemo(() => new Proxy({}, {
get: (_target, key) => fieldChain(schema, [], register, fieldRefs, { formValue, setFormValue, formErrors })[key]
}) as FieldChain<Schema>, [schema, formValue, formErrors])
}) as FieldChain<Schema>, [schema, setFormValue, formErrors])

return {
fields,
Expand Down
9 changes: 7 additions & 2 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Field, FieldRefs, RegisterFn, RegisterOptions } from '.'
import { z } from 'zod'

type AnyZodContainer = z.AnyZodObject | z.AnyZodTuple | z.ZodArray<any>

// Creates the type for the field chain by recusively travelling through the Zod schema
type recursiveFieldChain<Schema extends z.ZodType, LeafValue> =
z.infer<Schema> extends LeafValue ? z.infer<Schema>
: Schema extends z.AnyZodTuple ? { [K in keyof z.infer<Schema>]: FieldChain<Schema['_type'][K]> }
: Schema extends z.ZodArray<any> ? { [k: number]: FieldChain<Schema['_def']['type']> }
: Schema extends z.AnyZodObject ? { [K in keyof z.infer<Schema>]: FieldChain<Schema['shape'][K]> }
: Schema extends (z.ZodDefault<any> | z.ZodOptional<any> | z.ZodNullable<any>) ? FieldChain<Schema['_def']['innerType']>
: Schema extends (z.ZodEffects<any>) ? FieldChain<Schema['_def']['schema']>
: Schema extends (z.ZodDefault<AnyZodContainer> | z.ZodOptional<AnyZodContainer> | z.ZodNullable<AnyZodContainer>) ? FieldChain<Schema['_def']['innerType']>
: Schema extends (z.ZodEffects<AnyZodContainer>) ? FieldChain<Schema['_def']['schema']>
: LeafValue

export type FieldChain<Schema extends z.ZodType> = Field<Schema> & Required<recursiveFieldChain<Schema, {
Expand Down Expand Up @@ -42,6 +44,9 @@ export type RecursivePartial<T> = {
/** Same behaviour as Partial but does not affect arrays. */
export type PartialObject<T> = T extends any[] ? T : Partial<T>

/** Excludes undefined from a type, but keeps null */
export type NonUndefined<T> = T extends undefined ? never : T

export const unwrapZodType = (type: z.ZodType): z.ZodType => {
if (type instanceof z.ZodObject || type instanceof z.ZodArray) return type

Expand Down

0 comments on commit 384a1b0

Please sign in to comment.