폼의 다른 입력을 처리할 때 `use-form-hook` 모듈을 많이 사용했기 때문에 컴포넌트를 직접 제어하는 방법을 잊었습니다.
그래서 오랜만에 다시 입력을 처리해주는 커스텀 훅을 만들어보려고 합니다!
짜잔
const useInput = (
initialValue: string
): {
ref: MutableRefObject<HTMLInputElement>;
value: string;
isDirty: boolean;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onReset: () => void;
onFocus: () => void;
} => {
const ref = useRef() as MutableRefObject<HTMLInputElement>;
const (value, setValue) = useState(initialValue);
const (isDirty, setDirty) = useState(false);
const handler = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const onReset = () => setValue(initialValue);
const onFocus = () => {
if (ref.current) {
ref.current.focus();
}
};
useEffect(() => {
setDirty(!
!
value.length);
}, (value));
return { ref, value, isDirty, onChange: handler, onReset, onFocus };
};
export default useInput;
제네릭은 `useInput`의 인자인 `initialValue`로 서로 다른 타입을 입력할 수 있다는 전제하에 사용할 수 있지만 대부분의 텍스트(문자열)를 처리하기 때문에 타입은 `문자열`로 제한됩니다.
다양한 방식으로 대응하고 싶다면 제네릭 의약품 사용,
import { Dispatch, SetStateAction, useCallback, useState, ChangeEvent } from 'react';
type ReturnTypes<T> = (T, (e: ChangeEvent<HTMLInputElement>) => void, Dispatch<SetStateAction<T>>);
const useInput = <T>(initialData: T): ReturnTypes<T> => {
const (value, setValue) = useState(initialData);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setValue((e.target.value as unknown) as T);
}, ());
return (value, onChange, setValue);
};
export default useInput;
내가 그것을 바꿀 수 있는지 궁금합니다.
입력 값을 처리할 때 DOM에 직접 접근하여 값을 추출하느냐, React 컴포넌트에서 값을 조정해서 추출하느냐에 따라 비제어 컴포넌트(전자)와 제어 컴포넌트(후자)로 나눌 수 있다.
여기서 ref에 대한 직접 접근은 입력된 DOM(onFocus 메소드)에 집중하기 위해서만 사용되었고, DOM이 자율적으로 접근할 수 있도록 ref가 반환되었습니다.
값(`value`)을 추적하고 값이 변경될 때 특정 setter(`setDirty`)를 실행하기 위해 `useEffect`를 함께 사용했습니다.
이렇게 생성된 `useInput` hook을 원하는 뷰 페이지에 호출하여 사용하면,
import { FC, useState, useEffect, useCallback, SyntheticEvent, useRef, MutableRefObject } from 'react';
import { useUI } from '../ui/context';
import Input from '../ui/Input/Input';
import useInput from '../../lib/hooks/useInput';
import { Button } from '../ui';
const LoginView: FC = () => {
const {
ref: emailRef,
value: email,
onChange: onEmailChange,
onReset: onEmailReset,
onFocus: onEmailFocus,
} = useInput('');
const { value: password, onChange: onPasswordChange, onReset: onPasswordReset } = useInput('');
const (disabled, setDisabled) = useState(true);
const handleLogin = async (e: SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
try {
console.log({ email, password });
onEmailReset();
onPasswordReset();
} catch ({ errors }: any) {
...
} finally {
setDisabled(true);
onEmailFocus();
}
};
const handleValidation = useCallback(async () => {
const validEmail = /(a-z0-9)+@/.test(email);
const validPassword = /(A-Za-z0-9._%+-){8,}/.test(password);
setDisabled(!
validEmail || !
validPassword);
}, (email, password));
useEffect(() => {
handleValidation();
}, (handleValidation));
return (
<div>
<h2>로그인</h2>
<form onSubmit={handleLogin} ref={root}>
<Input ref={emailRef} value={email} onChange={onEmailChange} />
<Input value={password} onChange={onPasswordChange} />
<Button type="submit" disabled={disabled} loading={loading}>
로그인
</Button>
</form>
</div>
);
};
export default LoginView;
읽기 쉽고 여러 입력을 관리하기 위해 여러 번 재사용할 수 있습니다!
입력 타입이 파일일 때 아쉬운 점이 있다면 다른 로직이 필요하므로 useFiles`라는 커스텀 훅으로 여러 파일을 비동기적으로 병렬로 관리할 수 있는 방식으로 개발해야 할 것 같습니다.
나중에!
끝.