Created
January 6, 2023 22:08
-
-
Save temoncher/ab12c46f0d779e75be72dafe52073991 to your computer and use it in GitHub Desktop.
A more typesafe alternative to `react-hook-form`, useZodForm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState } from 'react'; | |
import { z } from 'zod'; | |
import reactLogo from './assets/react.svg'; | |
import './App.css'; | |
import { useZodForm, register } from './useZodForm'; | |
type ObjectKey = keyof any; | |
type Field<N extends ObjectKey, V> = { | |
name: N; | |
value: V; | |
onChange: (newValue: V) => void; | |
onBlur?: () => void; | |
}; | |
type RenderField<N extends ObjectKey, V> = (fieldApi: { | |
field: Field<N, V>; | |
}) => JSX.Element; | |
type ValidateForm<FV, T extends FV> = (formValues: FV) => formValues is T; | |
type UseZodFormInputOnlySchema<S extends z.ZodObject<any>> = { | |
schema: S; | |
}; | |
type UseZodFormInputValidateFn<S extends z.ZodObject<any>, T> = { | |
schema: S; | |
validateForm?: ValidateForm<z.infer<S>, T>; | |
}; | |
type UseZodFormOutputOnlySchema<S extends z.ZodObject<any>> = { | |
render: (formMarkup: { | |
[K in keyof S['_type']]: RenderField<K, S['_type'][K]>; | |
}) => JSX.Element; | |
handleSubmit: ( | |
submitFn: (validatedValues: z.infer<S>) => void | |
) => (event: React.FormEvent<HTMLFormElement>) => void; | |
}; | |
type UseZodFormOutputValidationFn<S extends z.ZodObject<any>, T> = { | |
render(formMarkup: { | |
[K in keyof S['_type']]: RenderField<K, S['_type'][K]>; | |
}): JSX.Element; | |
handleSubmit: ( | |
submitFn: (validatedValues: T) => void | |
) => (event: React.FormEvent<HTMLFormElement>) => void; | |
}; | |
export function useZodForm<S extends z.ZodObject<any>>( | |
input: UseZodFormInputOnlySchema<S> | |
): UseZodFormOutputOnlySchema<S>; | |
export function useZodForm<S extends z.ZodObject<any>, T>( | |
input: UseZodFormInputValidateFn<S, T> | |
): UseZodFormOutputValidationFn<S, T>; | |
export function useZodForm<S extends z.ZodObject<any>, T>({ | |
schema, | |
validateForm, | |
}: UseZodFormInputValidateFn<S, T>) { | |
const [formValues, setFormValues] = useState< | |
Record<string, unknown> | undefined | |
>(undefined); | |
const [touchedFields, setTouchedFields] = useState<Record<string, unknown>>( | |
{} | |
); | |
const [dirtyFields, setDirtyFields] = useState<Record<string, unknown>>({}); | |
return { | |
render(formMarkup: { | |
[K in keyof S['_type']]: RenderField<K, S['_type'][K]>; | |
}) { | |
return ( | |
<> | |
{Object.entries(formMarkup).map(([fieldKey, renderField]) => { | |
const onChange = (newValue: unknown) => { | |
setFormValues((prev) => ({ | |
...prev, | |
[fieldKey]: newValue, | |
})); | |
setDirtyFields((prev) => ({ | |
...prev, | |
[fieldKey]: true, | |
})); | |
}; | |
const onBlur = () => { | |
setTouchedFields((prev) => ({ ...prev, [fieldKey]: true })); | |
}; | |
return ( | |
<React.Fragment key={fieldKey}> | |
{renderField({ | |
field: { | |
name: fieldKey, | |
value: formValues?.[fieldKey], | |
onChange, | |
onBlur, | |
}, | |
fieldState: { | |
invalid: false, | |
isTouched: touchedFields[fieldKey], | |
isDirty: dirtyFields[fieldKey], | |
error: '', | |
}, | |
})} | |
</React.Fragment> | |
); | |
})} | |
</> | |
); | |
}, | |
handleSubmit(submitFn: (validatedValues: z.infer<S> | T) => void) { | |
return (event: React.FormEvent<HTMLFormElement>) => { | |
event.preventDefault(); | |
const htmlFormValues = Object.fromEntries(new FormData(event.target)); | |
const parsedFormValues = schema.parse(htmlFormValues); | |
if (!validateForm) { | |
submitFn(parsedFormValues); | |
return; | |
} | |
if (validateForm(parsedFormValues)) { | |
submitFn(parsedFormValues); | |
} | |
}; | |
}, | |
}; | |
} | |
export function register<N extends ObjectKey, V>(field: Field<N, V>) { | |
console.log('reg', field); | |
return { | |
...field, | |
onChange: (event: React.ChangeEvent<HTMLInputElement>) => { | |
field.onChange(event.target.value); | |
}, | |
}; | |
} | |
const schema = z.object({ | |
email: z.string().email(), | |
password: z.string(), | |
count: z.number(), | |
}); | |
function App() { | |
const [count, setCount] = useState(0); | |
const form = useZodForm({ schema }); | |
return ( | |
<form | |
onSubmit={form.handleSubmit((validatedValues) => { | |
console.log(validatedValues); | |
})} | |
> | |
{form.render({ | |
email: ({ field }) => <input type="email" {...register(field)} />, | |
password: ({ field }) => <input type="password" {...register(field)} />, | |
count: ({ field }) => <input type="number" {...register(field)} />, | |
})} | |
<button type="submit">Submit</button> | |
</form> | |
); | |
} | |
export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment