starkoora / wanted-pre-onboarding-challenge-fe-1

64 stars 1 forks source link

[질문] React-Hook-Form 사용 시 register 함수 Input 컴포넌트 내려줄 때 undefined issue #28

Closed jinyoung234 closed 1 year ago

jinyoung234 commented 1 year ago

질문

안녕하세요..! 리팩토링을 진행하다가 react-hook-form에 있는 input을 건들고 있는데, 다음과 같이 register 함수를 프레젠터에 내려주어 내려받은 함수를 Input에 다시 내려주는 형태입니다. 하지만 컨테이너에서 콘솔로 watch 함수로 호출하여 보니 id, pw가 undefined로 찍히게 됩니다. 원인을 알 수 없어 질문 남기게 되었습니다!

/** SignUp.container.tsx */
import React from 'react'
import {useForm} from 'react-hook-form'
import {getSignUp} from '../../api/auth'
import SignUpForm from '../../components/auth/SignUpForm'
import {IFormData} from '../../components/auth/SignUpForm/type'
import {SignUpProps} from '../../types'
import {emailRegex} from '../../utils/regex'

function SignUpContainer() {
  const {
    register,
    watch,
    handleSubmit,
    formState: {errors},
  } = useForm<IFormData>()
  const onValid = (data: IFormData) => {
    getSignUp({email: data.id, password: data.pw}).then(res => {
      if (res?.status === 200) window.location.href = 'auth/login'
    })
  }
  const signUpProps: SignUpProps = {
    form: {
      handleSubmit,
      onValid,
    },
    input: {
      register,
    },
  }
  return <SignUpForm {...signUpProps} />
}

export default SignUpContainer

비즈니스 로직을 내려주는 컨테이너 컴포넌트 입니다.

/* SignUpForm.tsx */
import React from 'react'
import {Form} from '../../../wrappers/common/FormWrapper'
import {SignUpProps} from '../../../types'

function SignUpForm({...signUpForm}: SignUpProps) {
  const {
    form: {handleSubmit, onValid},
    input: {register},
  } = signUpForm
  return (
    <>
      <Form handleSubmit={handleSubmit(onValid)}>
        <Form.Input type='text' id='id' {...register('id')} />
        <Form.Input type='password' id='pw' {...register('pw')} />
      </Form>
    </>
  )
}

export default SignUpForm

컨테이너에서 데이터를 내려 받아 사용하는 SignUpForm 컴포넌트(presenter) 입니다.

// type.ts
import {InputHTMLAttributes} from 'react'

type InputProps = InputHTMLAttributes<HTMLInputElement>

export type {InputProps}

// Input.tsx
import React from 'react'
import {InputProps} from './type'

function Input({id, type, ...rest}: InputProps) {
  console.log(rest)
  return <input {...rest} />
}

export default Input

register에 있는 attribute들과, id, type prop을 받는 Input 컴포넌트 입니다.

LucetTin5 commented 1 year ago

https://react-hook-form.com/api/useformcontext

저는 한 프로젝트에서 Input component에 methods를 내려줄 때 위의 form context방식을 적용했습니다.

starkoora commented 1 year ago

@jinyoung234

function SignUpForm({
    form: {handleSubmit, onValid},
    input: {register},
  }: SignUpProps) {

  return (
    <>

~구조분해 할당을 잘못 하신거 아닐까요?~

function SignUpForm(props) {

~이 형태로 받고 props로 들어오는 값을 보시면 될거 같습니다~

아 이 문제는 아니겠군요 (취소)

starkoora commented 1 year ago

console.log(rest)는 잘 찍히고 있나요?

jinyoung234 commented 1 year ago
import React from 'react'
import {UseFormRegister} from 'react-hook-form'
import {IFormData} from '../../auth/SignUpForm/type'
import {InputProps} from './type'

const Input = React.forwardRef<HTMLInputElement, InputProps & ReturnType<UseFormRegister<IFormData>>>(
  ({...rest}: InputProps, ref) => {
    return (
      <>
        <input ref={ref} {...rest} />
      </>
    )
  },
)

Input.displayName = 'input'

export default Input

공식문서 보면서 forwardRef로 바꾸니까 해결은 됬는데 정확한 원인은 잘 모르겠어서 좀 더 공부해봐야할 거 같습니다.. ㅜㅜ 혹시 괜찮으시다면 같이 고민해보면 좋을 거 같긴 한데 다른 분들 사정도 있으시니..! 일단 rest 찍히는지도 확인해보겠습니다!

starkoora commented 1 year ago

@jinyoung234 아하 forwardRef 문제가 맞을거 같아요 결국 react-hook-form 쪽에 해당 input에 대한 ref 값을 넘겨야 하는데 forwardRef로 넘기지 않으면 ref가 넘어가지 않기 때문에 useForm 입장에서는 입력받은 ref가 없는 상황일 것 같네요

jinyoung234 commented 1 year ago

아 Input의 ref를 useForm에 넘겨주는 원리여서 forwardRef를 통해 넘겨주는 걸로 이해해도 될까여?!

starkoora commented 1 year ago
image

https://react-hook-form.com/api/useform/register/

보시면 register의 리턴값 중에 ref도 있는데요 Form.Input에 register 함수만 넘기시고, Input 안쪽에서 {...register(foo)} 하게 되면 forwardRef 없이도 될것 같은 느낌적인 느낌이네요 @jinyoung234

jinyoung234 commented 1 year ago

그렇게 할 수 있으면 좋을거 같은데 input이 공통 컴포넌트라 id에도 쓰고 pw에도 사용하는데도 가능할까여 혹시..?!

그리고 한가지 더 여쭤보고 싶은게 있는데 지금 제가 컨테이너 - 프레젠터 패턴을 염두에 잡고 위 코드 대로 진행을 했습니다. 보시면 SignUpForm에서 Input으로 register를 한번 더 내려주게 되어 정확한 비즈니스 - 뷰 분리가 안된거 같아서 그런데 혹시 SignUpForm을 컨테이너 - 프레젠터로 한번 더 나눠야 할까요..?!ㅜㅜ 답변 힘드시면 괜찮습니다!

starkoora commented 1 year ago
jinyoung234 commented 1 year ago

아 타입을 그렇게 설정할 수도 있군여..! 수정해보겠습니다. 제가 생각한 프레젠터는 완전 뷰와 관련된 로직만 포함시키는게 목적이었어서 이 설계가 잘못된 설계라고 생각했는데, 혹시 제가 해당 패턴을 잘못 이해한건지 알고 싶습니다 ㅜㅜ!

zeromountain commented 1 year ago

props의 타입과 관련된 이슈라면, react-hook-form에서 제공하는 FormProvider 사용해보시는 것도 좋을거 같아요, FormProvider로 감싼 context 내에서 useForm의 method들을 공유할 수 있어서 props로 register를 전달하는 방법보다 괜찮지 않을까 생각합니다. @LucetTin5 님과 같은 맥락이네요

jinyoung234 commented 1 year ago

@LucetTin5 @zeromountain 감사합니다 참고하겠습니다!

young-st511 commented 1 year ago

답변이 매우 늦었지만 이전 프로젝트에서 정확히 같은 문제로 고생했어서 답변 남깁니다..! 멘토님 답변처럼 register 리턴에 ref가 있는게 문제더라구요.. 저는 폼타입을 제네릭으로 받아서 register를 통으로 받아 해결했었습니다..!

interface InputProp<T extends FieldValues> extends InputHTMLAttributes<HTMLInputElement> {
  label?: string | undefined;
  plHolder?: string | undefined;
  name: FieldPath<T>;
  register: UseFormRegister<T>;
  options?: RegisterOptions<T, FieldPath<T>>;
};

function Input<T extends FieldValues>({
  label,
  plHolder,
  name,
  type = 'text',
  required,
  register,
  options,
}: InputProp<T>) {
  return (
    <S.Input className="input-container">
      <label>
        {label && <div className="label">{label}</div>}
        <input
          {...register(name, options)}
          placeholder={plHolder}
          required={required}
          autoComplete="off"
          type={type}
        />
      </label>
    </S.Input>
  );
}
jinyoung234 commented 1 year ago

@young-st511 오 감사합니다 참고하겠습니다..!!