Skip to content

Instantly share code, notes, and snippets.

@temoncher
Created January 6, 2023 22:08
Show Gist options
  • Save temoncher/ab12c46f0d779e75be72dafe52073991 to your computer and use it in GitHub Desktop.
Save temoncher/ab12c46f0d779e75be72dafe52073991 to your computer and use it in GitHub Desktop.
A more typesafe alternative to `react-hook-form`, useZodForm
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