dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

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 }