Skip to content

Commit 76deb85

Browse files
feat: add watch to trigger hook (#526)
* feat: add watch to trigger hook * docs: improve comment by adding example
1 parent 1c429f6 commit 76deb85

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { Button, Stack } from '@chakra-ui/react';
2+
import { zodResolver } from '@hookform/resolvers/zod';
3+
import { useForm } from 'react-hook-form';
4+
import { z } from 'zod';
5+
6+
import {
7+
Form,
8+
FormField,
9+
FormFieldController,
10+
FormFieldLabel,
11+
} from '@/components/Form';
12+
import { getFieldPath } from '@/lib/form/getFieldPath';
13+
14+
import { useWatchToTrigger } from '.';
15+
16+
export default {
17+
title: 'Hooks/useWatchToTrigger',
18+
};
19+
20+
type FormType = z.infer<ReturnType<typeof formSchema>>;
21+
const formSchema = () =>
22+
z
23+
.object({ min: z.number(), default: z.number(), max: z.number() })
24+
.superRefine((val, ctx) => {
25+
if (val.min > val.default) {
26+
ctx.addIssue({
27+
code: 'custom',
28+
path: getFieldPath<FormType>('min'),
29+
message: 'The min should be <= to default',
30+
});
31+
}
32+
33+
if (val.default > val.max) {
34+
ctx.addIssue({
35+
code: 'custom',
36+
path: getFieldPath<FormType>('default'),
37+
message: 'The default should be <= to the max',
38+
});
39+
}
40+
});
41+
42+
export const WithoutHook = () => {
43+
const form = useForm({
44+
mode: 'onBlur',
45+
resolver: zodResolver(formSchema()),
46+
defaultValues: {
47+
min: 2,
48+
default: 4,
49+
max: 6,
50+
},
51+
});
52+
53+
return (
54+
<Form {...form}>
55+
<Stack>
56+
<FormField>
57+
<FormFieldLabel>Min</FormFieldLabel>
58+
<FormFieldController
59+
control={form.control}
60+
name="min"
61+
type="number"
62+
/>
63+
</FormField>
64+
<FormField>
65+
<FormFieldLabel>Default</FormFieldLabel>
66+
<FormFieldController
67+
control={form.control}
68+
name="default"
69+
type="number"
70+
/>
71+
</FormField>
72+
<FormField>
73+
<FormFieldLabel>Max</FormFieldLabel>
74+
<FormFieldController
75+
control={form.control}
76+
name="max"
77+
type="number"
78+
/>
79+
</FormField>
80+
<Button type="submit" variant="@primary">
81+
Submit
82+
</Button>
83+
</Stack>
84+
</Form>
85+
);
86+
};
87+
88+
export const WithHook = () => {
89+
const form = useForm({
90+
mode: 'onBlur',
91+
resolver: zodResolver(formSchema()),
92+
defaultValues: {
93+
min: 2,
94+
default: 4,
95+
max: 6,
96+
},
97+
});
98+
99+
useWatchToTrigger({ form, names: ['min', 'default', 'max'] });
100+
101+
return (
102+
<Form {...form}>
103+
<Stack>
104+
<FormField>
105+
<FormFieldLabel>Min</FormFieldLabel>
106+
<FormFieldController
107+
control={form.control}
108+
name="min"
109+
type="number"
110+
/>
111+
</FormField>
112+
<FormField>
113+
<FormFieldLabel>Default</FormFieldLabel>
114+
<FormFieldController
115+
control={form.control}
116+
name="default"
117+
type="number"
118+
/>
119+
</FormField>
120+
<FormField>
121+
<FormFieldLabel>Max</FormFieldLabel>
122+
<FormFieldController
123+
control={form.control}
124+
name="max"
125+
type="number"
126+
/>
127+
</FormField>
128+
<Button type="submit" variant="@primary">
129+
Submit
130+
</Button>
131+
</Stack>
132+
</Form>
133+
);
134+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useEffect } from 'react';
2+
3+
import { FieldPath, FieldValues, UseFormReturn } from 'react-hook-form';
4+
5+
/**
6+
* Use this hook to subscribe to fields and listen for changes to revalidate the
7+
* form.
8+
*
9+
* Using the form "onBlur" validation will validate the field you just updated.
10+
* But imagine a field that has to validate itself based on an another field update.
11+
* That's the point of this hook.
12+
*
13+
* Example: imagine those fields: `min`, `default`, `max`. The `min` should be
14+
* lower than the `default` and the `default` should lower than the `max`.
15+
* `min` is equal to 2, `default` is equal to 4 and `max` is equal to 6.
16+
* You update the `min` so the value is 5, the form (using superRefine and
17+
* custom issues) will tell you that the `min` should be lower than the default.
18+
* You update the `default` so the new value is 5.5. Without this hook, the
19+
* field `min` will not revalidate. With this hook, if you give the field name,
20+
* it will.
21+
*
22+
* @example
23+
* // Get the form
24+
* const form = useFormContext<FormType>();
25+
*
26+
* // Subscribe to fields validation
27+
* // If `default` changes, `min`, `default` and `max` will validate and trigger
28+
* // error if any.
29+
* useWatchToTrigger({ form, names: ['min', 'default', 'max']})
30+
*/
31+
export const useWatchToTrigger = <
32+
TFieldValues extends FieldValues = FieldValues,
33+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
34+
>(params: {
35+
form: Pick<UseFormReturn<TFieldValues>, 'watch' | 'trigger'>;
36+
names: Array<TName>;
37+
}) => {
38+
const { watch, trigger } = params.form;
39+
useEffect(() => {
40+
const subscription = watch((_, { name }) => {
41+
if (name && params.names.includes(name as TName)) {
42+
trigger(params.names);
43+
}
44+
});
45+
return () => subscription.unsubscribe();
46+
}, [watch, trigger, params.names]);
47+
};

0 commit comments

Comments
 (0)