React Hook Form + zod + 커스텀 input +  storybook

React Hook Form + zod + 커스텀 input + storybook

 
 
textField 컴포넌트. react hook form을 받아서 상태를 구현
 
 
입력 없을경우
notion image
 
focuse 시 하단 강조
notion image
 
값이 있을 경우 x 버튼과 하단 검정색
notion image
 
zod로 검증하여 에러생성.
notion image
 
import React, { useState } from 'react'; import IconButton from './IconButton'; import { XIcon } from '../icon/Icons'; import ErrorMessage from './ErrorMessage'; import { cn } from '@/lib/utils'; import { FieldErrors, UseFormRegister, UseFormResetField, UseFormSetFocus, UseFormWatch } from 'react-hook-form'; interface CommonTextField { errorMessage: FieldErrors<any>; placeholder: string; IsError: boolean; register: UseFormRegister<any>; id: string; watch: UseFormWatch<any>; resetField: UseFormResetField<any>; setFocus: UseFormSetFocus<any>; } export default function CommonTextField({ errorMessage, placeholder, IsError, id, register, watch, resetField, setFocus, }: CommonTextField) { const inputValue = watch(id); const [isFocused, setIsFocused] = useState(false); // 아이콘 클릭시 값삭제후 focus const onIconClick = () => { resetField(id); setFocus(id); }; const borderColor = isFocused ? 'border-emerald-400' : !inputValue ? 'border-gray-200' : 'border-black'; return ( <div> <div onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} className={cn('flex flex-row justify-between border-b border-primary text-primary', borderColor)}> <input className="grow outline-none" type="text" placeholder={placeholder} {...register(id)} /> {inputValue && ( <IconButton onClick={onIconClick}> <XIcon /> </IconButton> )} </div> <ErrorMessage className="min-h-[40px]">{errorMessage[id]?.message as string}</ErrorMessage> </div> ); }
 
import type { Meta, StoryObj } from '@storybook/react'; import CommonTextField from './CommonTextField'; import React from 'react'; import { boolean, string, z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; // inputField는 react-hook-form을 사용하기때문에 // story로 만들기 어려운 부분이 존재함 // zodResolver만 객체로 받게해서 // 에러메세지를 커스텀 할 수 있게 했음 // 하지만 react-hook-form을 넣어서 만들어서 재사용성이 떨어짐. const User = z.object({ firstName: z .string() .min(1, { message: '이름은 필수' }) .min(6, { message: '이름은 6이상 12이하' }) .max(12, { message: '이름은 6이상 12이하' }), }); const CommonTextFieldStory = ({ placeholder, zodResolverObj, }: { placeholder: string; zodResolverObj: z.ZodObject<any>; }) => { const { register, watch, resetField, setFocus, formState: { errors, isValid, isLoading, isSubmitted, isSubmitting }, } = useForm<FormInput>({ criteriaMode: 'all', resolver: zodResolver(zodResolverObj), mode: 'onBlur', }); return ( <form action=""> <CommonTextField errorMessage={errors} placeholder={placeholder} IsError={!(Object.keys(errors).length === 0)} register={register} id={'firstName'} watch={watch} resetField={resetField} setFocus={setFocus} /> </form> ); }; const meta = { title: 'Components/TextField/CommonTextField', component: CommonTextFieldStory, argTypes: { placeholder: { type: 'string', description: 'Placeholder' }, }, parameters: { layout: 'centered', }, tags: ['autodocs'], } satisfies Meta<typeof CommonTextFieldStory>; export default meta; type Story = StoryObj<typeof meta>; interface FormInput { inputField: string; } export const Primary2: Story = { args: { placeholder: 'Enter text', zodResolverObj: User, }, decorators: [ Story => ( <div className="w-[500px]"> <Story /> </div> ), ], };
 
 
아래는 서버 action을 활용했음
 
// app/actions.ts 'use server'; import { IFormInput } from '@/app/page'; export async function handleMyFormSubmit(data: IFormInput) { console.log({ data }); }
 
 
 
export interface IFormInput { firstName: string; } export default function Home() { const User = z.object({ firstName: z .string() .min(1, { message: '이름은 필수' }) .min(6, { message: '이름은 6이상 12이하' }) .max(12, { message: '이름은 6이상 12이하' }), }); const { register, handleSubmit, setError, getValues, watch, resetField, clearErrors, getFieldState, setValue, trigger, setFocus, formState: { errors, isValid, isLoading, isSubmitted, isSubmitting }, } = useForm<IFormInput>({ criteriaMode: 'all', resolver: zodResolver(User), mode: 'onBlur', }); const onMyFormSubmit = async (data: IFormInput) => { await handleMyFormSubmit(data); }; return ( <> <form onSubmit={handleSubmit(onMyFormSubmit)}> <CommonTextField errorMessage={errors} placeholder="텍스트를 입력해 주세요" IsError={!(Object.keys(errors).length === 0)} register={register} id={'firstName'} watch={watch} resetField={resetField} setFocus={setFocus} /> <button className="btn">전송</button> </form> </> ); }