use_form.ts (2725B)
1 import { ApiError } from '@/lib/apierror' 2 import { Dispatch, useCallback, useReducer, useState } from 'react' 3 4 export interface FormFields { 5 [key: string]: any 6 } 7 8 type FormFieldAction<T extends FormFields> = Partial<{ 9 [K in keyof T]: T[K] 10 }> 11 12 interface UpdateFieldOrDispatch<T extends FormFields> { 13 (fields: T): void 14 15 <K extends keyof T> (key: K, value: T[K]): void 16 } 17 18 /** 19 * 폼 필드 값을 관리하는 훅 20 * 21 * @param initial 초기 필드 값 22 * @returns [fields, update] fields: 현재 폼 필드 값, update: 필드 값을 업데이트하는 함수 23 */ 24 export const useFields = <T extends FormFields> (initial: T): [T, UpdateFieldOrDispatch<T>] => { 25 const [fields, dispatch] = useReducer((state: T, action: FormFieldAction<T>): T => { 26 return { ...state, ...action } 27 }, initial) 28 29 const update = useCallback<UpdateFieldOrDispatch<T>>( 30 (fieldsOrKey: T | keyof T, value?: unknown) => { 31 if (typeof fieldsOrKey === 'object') { 32 dispatch(fieldsOrKey) 33 } else { 34 dispatch({ [fieldsOrKey]: value } as FormFieldAction<T>) 35 } 36 }, 37 [], 38 ) 39 40 return [fields, update] 41 } 42 43 /** 44 * 폼을 관리하고 submit 이벤트를 처리하기 위한 훅 45 * 46 * TODO: 훅 대신 컴포넌트로 구현 47 */ 48 export const useForm = <T extends FormFields, Result = unknown> ( 49 input: string | { method: string, url: string | URL }, 50 initial: T, 51 onSuccess?: (result: Result) => void, 52 onError?: (status: number, error: ApiError) => void, 53 ): [fields: T, updateFields: UpdateFieldOrDispatch<T>, submit: () => void, 54 isLoading: boolean, result: Result | null, error: ApiError | null] => { 55 const [fields, updateFields] = useFields<T>(initial) 56 const [isLoading, setLoading] = useState(false) 57 const [result, setResult] = useState<Result | null>(null) 58 const [error, setError] = useState<ApiError | null>(null) 59 60 const { method, url } = typeof input === 'string' 61 ? { method: 'POST', url: input } 62 : input 63 64 const submit = useCallback(() => { 65 setLoading(true) 66 setResult(null) 67 setError(null) 68 69 !(async () => { 70 const res = await fetch(url, { 71 method: method, 72 headers: { 73 'Content-Type': 'application/json', 74 }, 75 body: JSON.stringify(fields), 76 }) 77 78 const data = await res.json() 79 setLoading(false) 80 81 if (!res.ok) { 82 setError(data) 83 onError?.(res.status, data) 84 return 85 } 86 87 setResult(data) 88 onSuccess?.(data) 89 })().catch((e) => { 90 console.error('useForm: unexpected error:', e) 91 setError({ code: 'internal_error', message: 'Internal error' }) 92 }) 93 }, [onSuccess, fields, input]) 94 95 return [fields, updateFields, submit, isLoading, result, error] 96 }