ã¯ããã«
ãã¡ãã¯ãã¤ã»ã«ãã¯ããã¸ã¼ãº Advent Calendar 2022ã® 24 æ¥ç®ã®è¨äºã§ãã
åæ¥ã®è¨äºã¯ç°ä¸ããã®ãç°å¢æ§ç¯ãã³ãã³ãã§ã¾ã¨ãã¦ã¿ããã®è¨äºã§ããã
â ããã«ã¡ã¯ï¼ ãã¯ããã¸ã¼æ¦ç¥æ¬é¨ éçºäºé¨ã®å°æã§ãã
â èªåãæ å½ããããã¸ã§ã¯ãã§ã¯ãå¼ç¤¾ã§åãã¦ããªãã¼ã·ã§ã³ã©ã¤ãã©ãªã¨ã㦠Zod ã使ç¨ããReact-Hook-Form à Zod ã®æ§æã§ãã©ã¼ã ãä½æãã¾ããã
â æ¬è¨äºã§ã¯ãå®éã«ããã¸ã§ã¯ãã§å®è£ ããäºä¾ãç´¹ä»ãããã¨æãã¾ãã
âReact-Hook-Form à ããªãã¼ã·ã§ã³ã©ã¤ãã©ãªã®æè¡é¸å®ã«è¿·ã£ã¦ããæ¹ããã¾ãããããåèã«ãªãã°å¹¸ãã§ãã
- ã¯ããã«
- 対象èªè
- React-Hook-Form ã¨ã¯
- Zod ã¨ã¯
- ãªã React-Hook-Form ã¨ããªãã¼ã·ã§ã³ã©ã¤ãã©ãªãçµã¿åãããã®ã
- Zod ã®é¸å®çç±
- React-Hook-Form à Zod ã®åºæ¬çãªä½¿ç¨ä¾
- å®éã«ããã¸ã§ã¯ãã§å®è£ ããäºä¾
- ãããã«
対象èªè
æ¬è¨äºã¯ãReact-Hook-Form ã使ã£ããã¨ã¯ããããããªãã¼ã·ã§ã³ã©ã¤ãã©ãªã«æ©ãã§ããæ¹ã対象ã¨ãã¦ãã¾ãã
ãã®ãããReact-Hook-Formã»Zod ã®åºæ¬çãªä½¿ãæ¹ã«ã¤ãã¦ã¯èª¬æãçç¥ãã¦ãã¾ãã
0 ããå¦ã³ããæ¹ã¯ãå
¬å¼ã»å¥è¨äºããä¸èªã®ä¸ãæ¬è¨äºãèªã¿é²ãã¦ããã ããã°å¹¸ãã§ãã
React-Hook-Form ã¨ã¯
â ãæ§è½ãæè»æ§ãæ¡å¼µæ§ã«åªãããã©ã¼ã ã¨ã使ããããããªãã¼ã·ã§ã³ããæ²ãã¦ãããReact åãã®ãã©ã¼ã 管çã©ã¤ãã©ãªã§ãã â
Zod ã¨ã¯
https://github.com/colinhacks/zod
â Zod ã¨ã¯ãTypeScript First ãªããªãã¼ã·ã§ã³ã©ã¤ãã©ãªã§ãã
React ç Ruby on Rails ã¨ãè¨ããããã«ã¹ã¿ãã¯ãã¬ã¼ã ã¯ã¼ã¯ã®Blitz ã§ã使ç¨ããã¦ããããã§ãã â
ãªã React-Hook-Form ã¨ããªãã¼ã·ã§ã³ã©ã¤ãã©ãªãçµã¿åãããã®ã
React-Hook-Form ã«ã¯ãã«ã¹ã¿ã ãã¸ãã¯ãçµãããã«ãvalidate API ãç¨æããã¦ãããèªç±ã«ããªãã¼ã·ã§ã³ãã¸ãã¯ãä½æãããã¨ãã§ãã¾ãã
â ããããä»ã®ãã©ã¼ã ã®å¤ãåç §ãããããªè¤éãªããªãã¼ã·ã§ã³ãå®ç¾ãããã¨ããã¨ãã©ããã¦ãã³ã³ãã¼ãã³ãå´ã«ããªãã¼ã·ã§ã³ãã¸ãã¯ãæ¸ããªãã¨ãããªããªãããã¸ãã¯ãæ£å¨ãã¦ãã¾ããã¨ã§ã以ä¸ã®ãããªåé¡ãçºçãã¦ãã¾ããã â
ããªãã¼ã·ã§ã³ã©ã¤ãã©ãªæªä½¿ç¨æã®åé¡ç¹
- 管çããã¥ãã
- ãã¹ããæ¸ãã¥ãã
- ããªãã¼ã·ã§ã³ãã¸ãã¯ã使ãåãã¥ãã
å ·ä½ä¾
以ä¸ã¯ãæ¬è¨äºå
ã§ç´¹ä»ããäºä¾ 2ãããªãã¼ã·ã§ã³ã©ã¤ãã©ãªã使ç¨ããªãã§æ¸ãç´ããä¾ã§ãã
status ã price 㨠reason ã®ããªãã¼ã·ã§ã³æã«åç
§ããããããReact-Hook-Form ã® API ã使ç¨ããªãã¦ã¯ãªãããApp ã³ã³ãã¼ãã³ãå
ã«ããªãã¼ã·ã§ã³ãã¸ãã¯ãæ¸ããªãã¨ãããªããªã£ã¦ãã¾ãã
以ä¸ã³ã¼ãã®ãã¢ç°å¢ã¯ãã¡ãã
import { useForm } from 'react-hook-form' type Status = 'OK' | 'NG' type Schema = { status: Status price: number reason: string } const validateStatus = (status?: Status) => { if (!status) return 'é¸æãã¦ãã ãã' return true } const App = () => { const { register, getValues, handleSubmit, formState: { errors }, } = useForm<Schema>() // statusã®å¤ãåç §ããããã«ãReact-Hook-Formã®getValues APIã使ç¨ããªãã¨ãããªãããã // ã³ã³ãã¼ãã³ãå´ã«ããªãã¼ã·ã§ã³ãã¸ãã¯ãæ¸ããªãã¨ãããªããªã£ã¦ãã const validatePrice = (price: number) => { const status = getValues('status') if (status === 'OK' && (Number.isNaN(price) || price <= 0)) return 'OKã®å ´åã¯1以ä¸ã®ä¾¡æ ¼ãå ¥åãã¦ãã ãã' } const validateReason = (reason: string) => { const status = getValues('status') if (status === 'NG' && !reason) return 'NGã®å ´åã¯çç±ãå ¥åãã¦ãã ãã' } return ( <form onSubmit={handleSubmit((d) => console.log(d))}> <label> OK <input type="radio" value="OK" {...register('status')} /> </label> <label> NG <input type="radio" value="NG" {...register('status', { validate: validateStatus, })} /> </label> {errors.status?.message && <p>{errors.status?.message}</p>} <input type="number" {...register('price', { valueAsNumber: true, validate: validatePrice, })} /> {errors.price?.message && <p>{errors.price?.message}</p>} <input {...register('reason', { validate: validateReason, })} /> {errors.reason?.message && <p>{errors.reason?.message}</p>} <input type="submit" /> </form> ) }
管çããã¥ãã
ã³ã³ãã¼ãã³ãã«ããªãã¼ã·ã§ã³ãã¸ãã¯ãå®ç¾©ããã¦ãã¾ã£ã¦ãããããã©ã®ã³ã³ãã¼ãã³ãã§ã©ã®ããªãã¼ã·ã§ã³ãã¸ãã¯ã使ç¨ããã¦ãããã管çã»ææ¡ããã®ãé£ãããªãã¾ãã
ãã®çµæãããã¸ã§ã¯ãå
ã«è¤æ°åããããªããªãã¼ã·ã§ã³ãã¸ãã¯ãå®ç¾©ãããçã®åé¡ãçºçãããã¨ãããã¾ãã
ãã¹ããã¥ãã
ããªãã¼ã·ã§ã³ãã¸ãã¯ãåãåºãã¦ããªãã®ã§ãåä½ãã¹ãã§ããªãã¼ã·ã§ã³ãã¸ãã¯ããã¹ããããã¨ãã§ãããã¤ã³ã¿ã©ã¯ã·ã§ã³ãã¹ãçãç¨ãããªãã¨ãã¹ãã§ããªããªã£ã¦ãã¾ãã
ããªãã¼ã·ã§ã³ãã¸ãã¯ã使ãåãã¥ãã
ããªãã¼ã·ã§ã³ãã¸ãã¯ãåãåºãã¦ããªããããç´æ¥ä½¿ãåããã§ãã¾ããã
ä»åã¯ãããã®åé¡ã«å¯¾å¿ãããããããªãã¼ã·ã§ã³ã©ã¤ãã©ãªã使ç¨ãããã¨ã«ãã¾ããã
ä½è«
ãã®ä¾ã§ã¯ãé«éé¢æ°ã使ããã¨ã§ããªãã¼ã·ã§ã³ãã¸ãã¯ãåé¢ããåé¡è§£æ±ºãããã¨ãå¯è½ã§ãã
ã§ããã常ã«ææ°ã®ç¶æ
ã§åã¬ã³ããªã³ã°ãçºçãããå¿
è¦ããããããæå³ããªããã°ããControlled Componentsã«ããªãã¨ãããªããã¨ããä¸è¦ãªåã¬ã³ããªã³ã°ãèªçºããããã©ã¼ãã³ã¹ã®æªåãæãå¯è½æ§ãããããæ¨å¥¨ããã¾ããã
èå³ãããæ¹ã¯ãã¡ãã®ã³ã¼ãä¾ããåç
§ãã ããã
â â
Zod ã®é¸å®çç±
ä»åããªãã¼ã·ã§ã³ã©ã¤ãã©ãªãé¸å®ããã«ããã£ã¦ã¯ããã¡ãã®è¨äºãåèã«ä»¥ä¸ã®ãã¨ãèæ ®ãã¾ããã â
èæ ®ç¹
- React-Hook-Form ã«å¯¾å¿ãã¦ããã
- Typescript 対å¿(schema ããåãèªåçæã§ããã)
- github ã®ã¹ã¿ã¼æ°
- ããã¥ã¡ã³ããè±å¯ã
- æè¡çææ¦(ã¡ã³ãã¼ãæªçµé¨ã®ã©ã¤ãã©ãªã使ããã)
ä¸è¨ãèæ ®ããçµæãYupã»Zodã»Superstruct ãæ®ãããããã¥ã¡ã³ãã®è±å¯ããã»ãçæãããåã®å³æ ¼ããã»ãæè¡çææ¦ãã®è¦³ç¹ãèæ ®ã㦠Zod ãæ¡ç¨ãã¾ããã
ããã¥ã¡ã³ãã®è±å¯ã | åã®å³æ ¼ã | æè¡çææ¦ | |
---|---|---|---|
Yup | â | â³ | Ã |
Zod | â³ | â | â |
Superstruct | Ã | â³ | â |
React-Hook-Form à Zod ã®åºæ¬çãªä½¿ç¨ä¾
ã¾ããäºä¾ã«å ¥ãåã«åºæ¬ç㪠React-Hook-Form à Zod ã®ä½¿ç¨ä¾ãå ¬å¼ã®ä¾ãå¼ç¨ãã¦ç´¹ä»ãã¾ãã â
ã³ã¼ãä¾
useForm ã® resolver ã« zodResolver + schema ã渡ãã ãã§ãç°¡åã«ããªãã¼ã·ã§ã³ãå®ç¾ã§ãã¾ãã
以ä¸ã³ã¼ãã®ãã¢ç°å¢ã¯ãã¡ãã â
import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import * as z from 'zod' const schema = z.object({ name: z.string().min(1, { message: 'Required' }), age: z.number().min(10), }) type Schema = z.infer<typeof schema> const App = () => { const { register, handleSubmit, formState: { errors }, } = useForm<Schema>({ resolver: zodResolver(schema), // zodResolver + schema }) return ( <form onSubmit={handleSubmit((d) => console.log(d))}> <input {...register('name')} /> {errors.name?.message && <p>{errors.name?.message}</p>} <input type="number" {...register('age', { valueAsNumber: true })} /> {errors.age?.message && <p>{errors.age?.message}</p>} <input type="submit" /> </form> ) }
ãã¹ã
Zod ã® Schema ãç¨ãããã¹ããæ¸ãã¦ã¿ã¾ãã
æ£å¸¸ç³»ã®å ´åã¯ãparse ããå¤ãã¢ãµã¼ã·ã§ã³ãã¾ãã
ç°å¸¸ç³»ã®å ´åã¯ãtoThrow ã¡ã½ããã使ç¨ããã¨ã©ã¼ãã¹ã«ã¼ãã¦ããããã¹ã«ã¼ãããã¨ã©ã¼ã®ä¸ã«ç¹å®ã®æè¨ãããããã¢ãµã¼ã·ã§ã³ãã¦ãã¾ãã
â
describe('schema', () => { describe('valid', () => { it('returns input', () => { const name = 'ãã¹ã太é' const age = 10 const input = { name, age, } const actual = schema.parse(input) expect(actual).toEqual(input) }) }) describe('inValid', () => { describe('name is empty string', () => { it('throws error', () => { const name = '' const age = 8 const input = { name, age, } expect(() => schema.parse(input)).toThrow('Required') }) }) describe('age is less than 10', () => { it('throw error', () => { const name = 'ãã¹ã太é' const age = 9 const input = { name, age, } expect(() => schema.parse(input)).toThrow() }) }) }) })
å®éã«ããã¸ã§ã¯ãã§å®è£ ããäºä¾
ããããã¯å®éã«ããã¸ã§ã¯ãã§å®è£ ããäºä¾ãç´¹ä»ãã¦ããã¾ãã â
äºä¾ 1: å¥ã ã®å ¥åå¤ã¨ãã¦ããéµä¾¿çªå·ã 1 ã¤ã®æååã«å¤æãã
ã¾ãã¯ç°¡åãªäºä¾ã¨ãã¦ãZod ã®transform APIã使ç¨ãã¦ãhandleSubmit ã§åãåãããã©ã¼ã ã®å¤ãä»»æã®å½¢ã«å¤æããäºä¾ãç´¹ä»ãã¾ãã
以ä¸ã³ã¼ãã®ãã¢ç°å¢ã¯ãã¡ãã
ã³ã¼ãä¾
3 æ¡ã4 æ¡ã§å¥ã ã«å ¥åãããéµä¾¿çªå·ã transform API ã使ã£ã¦å¤æããhandleSubmit 㧠7 æ¡ã® éµä¾¿çªå· ã¨ãã¦åãåã£ã¦ãã¾ãã â
import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' import * as z from 'zod' /** éµä¾¿çªå·ã®ä¸ä¸æ¡ï¼éµä¾¿å¥çªå·ï¼ */ const firstThreeDigitsSchema = z .string() .regex(/^\d{3}$/, { message: '3æ¡ã®æ°å¤ãå ¥åãã¦ãã ããã' }) /** éµä¾¿çªå·ã®ä¸åæ¡ï¼çºåçªå·ï¼ */ const lastFourDigitsSchema = z .string() .regex(/^\d{4}$/, { message: '4æ¡ã®æ°å¤ãå ¥åãã¦ãã ããã' }) export const zipCodeSchema = z .object({ firstThreeDigits: firstThreeDigitsSchema, lastFourDigits: lastFourDigitsSchema, }) // 7æ¡ã®éµä¾¿çªå·ãçæ .transform( ({ firstThreeDigits, lastFourDigits }) => firstThreeDigits + lastFourDigits, ) type InputSchema = z.input<typeof zipCodeSchema> // transformåã§æ¨è« type OutputSchema = z.output<typeof zipCodeSchema> // transformå¾ã§æ¨è« const App = () => { const { register, handleSubmit, formState: { errors }, } = useForm<InputSchema>({ resolver: zodResolver(zipCodeSchema), }) return ( <form onSubmit={handleSubmit((_d) => { // 7æ¡ã®éµä¾¿çªå·ãåãåãã // handleSubmitã®å¼æ°ã¯InputSchemaã§æ¨è«ããã¦ãã¾ãã®ã§ãOutputSchemaã«åã¢ãµã¼ã·ã§ã³ããå¿ è¦ããã const d = (_d as unknown) as OutputSchema console.log(d) })} > <input type="text" maxLength={3} {...register('firstThreeDigits')} /> {errors.firstThreeDigits?.message && ( <p>{errors.firstThreeDigits?.message}</p> )} - <input type="text" maxLength={4} {...register('lastFourDigits')} /> {errors.lastFourDigits?.message && ( <p>{errors.lastFourDigits?.message}</p> )} <input type="submit" /> </form> ) }
â z.input, z.output ã使ç¨ãããã¨ã§ãããããå¤æåå¾ã®åãåå¾ãããã¨ãã§ãã¾ãã
â 注æããªããã°ãããªãã®ã¯ãhandleSubmit å ã§åãåããå¤ã¯å¤æåã®åã§æ¨è«ããã¦ãã¾ããã¨ã§ãã
ããã¯ãReact-Hook-Form å´ã®åé¡ã®ããã§ãæ¢ã«Issueãçºè¡ããã¦ãããv8 ã§ä¿®æ£äºå®ã®ããã§ãã®ã§ãç¾å¨ã¯ OutputSchema ã§åã¢ãµã¼ã·ã§ã³ãããããªãç¶æ ã§ãã â
ãã¹ã
transform API ã使ããã¨ã§ handleSubmit å ã§å®è¡ããªããã°ãããªãå¤æãã¸ãã¯ã Schema ã«å¯ãããã¨ãã§ããã®ã§ãããªãã¼ã·ã§ã³ã¨ã¾ã¨ãã¦ãã¹ããè¡ããã¨ãã§ãã¾ãã â
describe('parse', () => { describe('when success', () => { const data = { firstThreeDigits: '123', lastFourDigits: '4567', } const expected = '1234567' it('adds sevenDigits', () => { const actual = zipCodeSchema.parse(data) expect(actual).toEqual(expected) }) }) describe('when an error occurs in the first three digits', () => { const data = { firstThreeDigits: '1234', lastFourDigits: '5678', } it('throws error', () => { expect(() => zipCodeSchema.parse(data)).toThrow( '3æ¡ã®æ°å¤ãå ¥åãã¦ãã ãã', ) }) }) describe('when an error occurs in the first three digits', () => { const data: InputSchema = { firstThreeDigits: '123', lastFourDigits: '45678', } it('throws error', () => { expect(() => zipCodeSchema.parse(data)).toThrow( '4æ¡ã®æ°å¤ãå ¥åãã¦ãã ãã', ) }) }) })
äºä¾ 2: ãã©ã¼ã ã®é ç®ãçµã¿åããã¦ããªãã¼ã·ã§ã³ãè¡ã
次ã«ãZod ã®refine APIã使ç¨ãã¦ãåãã©ã¼ã ã®é ç®ãçµã¿åãããããªãã¼ã·ã§ã³ãä½æããäºä¾ãç´¹ä»ãã¾ãã
â
ã³ã¼ãä¾
以ä¸ã®ä¾ã§ã¯ãstatus ã®å¤ã«ãã£ã¦ãé©ç¨ããããªãã¼ã·ã§ã³ãåãã¦ãã¾ãã â 以ä¸ã³ã¼ãã®ãã¢ç°å¢ã¯ãã¡ãã
import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' import * as z from 'zod' export const schema = z .object({ status: z.enum(['OK', 'NG'], { errorMap: () => ({ message: 'é¸æãã¦ãã ããã' }), }), price: z.number().or(z.nan()), reason: z.string(), }) .refine( (val) => val.status === 'NG' || (!Number.isNaN(val.price) && val.price > 0), { message: 'OKã®å ´åã¯1以ä¸ã®ä¾¡æ ¼ãå ¥åãã¦ãã ãã', path: ['price'], }, ) .refine((val) => val.status === 'OK' || val.reason, { message: 'NGã®å ´åã¯çç±ãå ¥åãã¦ãã ãã', path: ['reason'], }) type InputSchema = z.input<typeof schema> const App = () => { const { register, handleSubmit, formState: { errors }, } = useForm<InputSchema>({ resolver: zodResolver(schema), }) return ( <form onSubmit={handleSubmit((d) => console.log(d))}> <label> OK <input type="radio" value="OK" {...register('status')} /> </label> <label> NG <input type="radio" value="NG" {...register('status')} /> </label> {errors.status?.message && <p>{errors.status?.message}</p>} <input type="number" {...register('price', { valueAsNumber: true, })} /> {errors.price?.message && <p>{errors.price?.message}</p>} <input {...register('reason')} /> {errors.reason?.message && <p>{errors.reason?.message}</p>} <input type="submit" /> </form> ) }
â status ã OK ã®å ´åã¯ãprice ã®ã¿ 1 å以ä¸ã®å ¥åãå¿ é ã¨ãã¦ãã¾ãã status ã NG ã®å ´åã¯ãreason ã®ã¿ 1 æå以ä¸ã®å ¥åãå¿ é ã¨ãã¦ãã¾ãã
refine API ã¯ãã«ã¹ã¿ã ããªãã¼ã·ã§ã³ãèªç±ã«çµããã¨ãã§ãã第 1 å¼æ°ã«æ¸¡ããã³ã¼ã«ããã¯é¢æ°ã§ Falsy ãªå¤ãè¿ãã°ã¨ã©ã¼ã«ãªãã ãã®æ¯è¼çèªç±åº¦ã®é«ã API ã§ãã
第 2 å¼æ°ã«ã¯ããªãã¸ã§ã¯ãã渡ããã¨ãã§ããã¨ã©ã¼ã¡ãã»ã¼ã¸ããã¨ã©ã¼ãçºè¡ãã path ãè¨å®ãããã¨ãã§ãã¾ãã
ãã¹ã
æ¸ãããã¹ããç´¹ä»ãã¾ãã â
describe('parse', () => { describe('when valid', () => { describe('when status is OK and price is positive int', () => { it('returns input', () => { const input = { status: 'OK', price: 500, reason: '', } const actual = schema.parse(input) expect(actual).toEqual(input) }) }) describe('when status is NG and reason is not empty string', () => { it('returns input', () => { const input = { status: 'NG', price: NaN, reason: 'ãã¹ã', } const actual = schema.parse(input) expect(actual).toEqual(input) }) }) }) describe('when inValid', () => { describe('when status is OK and price is NaN', () => { it('throws error', () => { const input = { status: 'OK', price: NaN, reason: 'ãã¹ã', } expect(() => schema.parse(input)).toThrow( 'OKã®å ´åã¯1以ä¸ã®ä¾¡æ ¼ãå ¥åãã¦ãã ãã', ) }) }) describe('when status is OK and price is 0', () => { it('throws error', () => { const input = { status: 'OK', price: 0, reason: 'ãã¹ã', } expect(() => schema.parse(input)).toThrow( 'OKã®å ´åã¯1以ä¸ã®ä¾¡æ ¼ãå ¥åãã¦ãã ãã', ) }) }) describe('when status is NG and reason is empty string', () => { it('throws error', () => { const input = { status: 'NG', price: 1, reason: '', } expect(() => schema.parse(input)).toThrow( 'NGã®å ´åã¯çç±ãå ¥åãã¦ãã ãã', ) }) }) }) })
äºä¾ï¼: è¤æ°ã®é ç®ã«å¯¾ãã¦ã¨ã©ã¼ãè¨å®ãã
æå¾ã«ãZod ã®superRefine APIã使ç¨ãã¦ããã©ã¼ã å ã®è¤æ°ã®é ç®ã«ã¨ã©ã¼ãè¨å®ããäºä¾ãç´¹ä»ãã¾ãã
â
ã³ã¼ãä¾
以ä¸ã®ä¾ã§ã¯ãæ¼ããã submit ãã¿ã³ã«å¿ãã¦ãããªãã¼ã·ã§ã³ã®å 容ãå¤åãããã¨ã©ã¼ã«ãªã£ãå ´å㯠priceã»reason ã©ã¡ãã«ãã¨ã©ã¼ã¡ãã»ã¼ã¸ã表示ããã¦ãã¾ãã â 以ä¸ã³ã¼ãã®ãã¢ç°å¢ã¯ãã¡ãã
import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' import * as z from 'zod' export const schema = z .object({ status: z.enum(['OK', 'NG'], { errorMap: () => ({ message: 'é¸æãã¦ãã ããã', }), }), price: z.number().or(z.nan()), reason: z.string(), }) .superRefine((val, ctx) => { if (val.status === 'OK' && (Number.isNaN(val.price) || val.price <= 0)) { ctx.addIssue({ code: 'custom', message: 'OKã®å ´åã¯1以ä¸ã®ä¾¡æ ¼ãå ¥åãã¦ãã ãã', path: ['price'], }) ctx.addIssue({ code: 'custom', message: 'NGã®å ´åã¯çç±ãå ¥åãã¦ãã ãã', path: ['reason'], }) } }) .superRefine((val, ctx) => { if (val.status === 'NG' && !val.reason) { ctx.addIssue({ code: 'custom', message: 'OKã®å ´åã¯1以ä¸ã®ä¾¡æ ¼ãå ¥åãã¦ãã ãã', path: ['price'], }) ctx.addIssue({ code: 'custom', message: 'NGã®å ´åã¯çç±ãå ¥åãã¦ãã ãã', path: ['reason'], }) } }) type Schema = z.input<typeof schema> const App = () => { const { register, setValue, handleSubmit, formState: { errors }, } = useForm<Schema>({ resolver: zodResolver(schema), }) return ( <form onSubmit={handleSubmit((d) => { console.log(d) })} > <input type="number" {...register('price', { valueAsNumber: true, })} /> {errors.price?.message && <p>{errors.price?.message}</p>} <input {...register('reason')} /> {errors.reason?.message && <p>{errors.reason?.message}</p>} <input type="submit" value="OK" onClick={() => setValue('status', 'OK')} /> <input type="submit" value="NG" onClick={() => setValue('status', 'NG')} /> </form> ) }
â superRefine API ã使ãã¨ã第 1 å¼æ°ã«æ¸¡ããã³ã¼ã«ããã¯é¢æ°ã®ç¬¬ 2 å¼æ°ã« ctx ãåãåããã¨ãã§ãããããã addIssue ã¡ã½ããã使ã£ã¦ã¨ã©ã¼ãè¨å®ãããã¨ãã§ãã¾ãã ã¾ããaddIssue ãè¤æ°å®è¡ããã°ãã®æ°ã ãã¨ã©ã¼ãè¨å®ãããã¨ãã§ãã¾ãã â
ãã¹ã
æ¸ãããã¹ããç´¹ä»ãã¾ãã ä»åã¯ç°å¸¸ç³»ã§ 2 ã¤ã®ã¨ã©ã¼ã¡ãã»ã¼ã¸ã表示ããã¦ãããã¨ãã¢ãµã¼ã·ã§ã³ãã¦ãã¾ãã â
describe('when valid', () => { describe("status is 'OK'", () => { const input = { status: 'OK', price: 1, reason: '', } it('returns input', () => { const actual = schema.parse(input) expect(actual).toEqual(input) }) }) describe("status is 'NG'", () => { const input = { status: 'NG', price: NaN, reason: 'ãã¹ã', } it('returns input', () => { const actual = schema.parse(input) expect(actual).toEqual(input) }) }) }) describe('when invalid', () => { describe("status is 'OK' and price is NaN", () => { const input = { status: 'OK', price: NaN, reason: '', } it('throws error', () => { expect(() => schema.parse(input)).toThrow( 'OKã®å ´åã¯1以ä¸ã®ä¾¡æ ¼ãå ¥åãã¦ãã ãã', ) }) it('throws error', () => { expect(() => schema.parse(input)).toThrow( 'NGã®å ´åã¯çç±ãå ¥åãã¦ãã ãã', ) }) }) describe("status is 'NG' and reason is empty string", () => { const input = { status: 'NG', price: 0, reason: '', } it('throws error', () => { expect(() => schema.parse(input)).toThrow( 'OKã®å ´åã¯1以ä¸ã®ä¾¡æ ¼ãå ¥åãã¦ãã ãã', ) }) it('throws error', () => { expect(() => schema.parse(input)).toThrow( 'NGã®å ´åã¯çç±ãå ¥åãã¦ãã ãã', ) }) }) })
ãããã«
åãã¦å®åã§ä½¿ã£ã¦ã¿ã¦ãé¸å®å½æã«ã¯è¦ãã¦ããªãã£ã React-Hook-Form à Zod ã®ä¾¿å©ãªä½¿ç¨æ³ãããããã¨è¦ãã¦ãã¾ããã
å人çã«ã¯ããã¯ãããªãã¼ã·ã§ã³ãã¸ãã¯ãåé¢ã§ãããã¨ã§ãã©ã®ãã¿ã¼ã³ã§ãæ¯è¼çç°¡åã«ããªãã¼ã·ã§ã³ãã¸ãã¯ã»å¤æãã¸ãã¯ã®é¨åãåä½ãã¹ãã§æ¸ããã¨ãã§ããããã«ãªã£ãã®ãæé«ã® DX ã§ããã
ä»å¾ã¯ããç°å¢å¤æ°ã®èªã¿è¾¼ã¿ãã»ãå¤é¨ãã¼ã¿ã® parseãçã®ããã«ãReact-Hook-Form ã¨çµã¿åããã使ç¨æ³ä»¥å¤ã§ããç©æ¥µçã« Zod ãå°å ¥ãã¦ãããã¨æã£ã¦ãã¾ãã
æå¾ã«ããã¤ã»ã«ã§ã¯ã¨ã³ã¸ãã¢ãåéãã¦ãã¾ããå°ãã§ãæ°ã«ãªã£ãæ¹ã¯ãã²ãå¿åãå¾ ã¡ãã¦ãã¾ãã
herp.careers â
ææ¥ã® ãã¤ã»ã«ãã¯ããã¸ã¼ãº Advent Calendar 2022 㯠éåããã«ãã ãRender Props Callback Hell ã®è§£æ¶ã ã§ãã