브라우저는 폼을 다루기 위한 내장 API를 가지고 있다. 브라우저의 내장 API는 웹 개발 요구사항에 따라 한계를 가진다. 커스텀 오류 메세지를 보여준다거나 폼을 제출하기 하기 전에 입력값을 검증하는 등의 행위는 내장 API만으로는 구현이 까다롭다.
폼 요구사항을 구현하고 Custom Hook과 Reducer로 재활용이 가능하도록 구현해보겠다
아래는 재사용을 고려하지 않은 코드이다.
import { useState } from 'react';
const SimpleInput = (props) => {
const [enteredName, setEnteredName] = useState('');
const [enteredNameTouched, setEnteredNameTouched] = useState(false);
const [enteredEmail, setEnteredEmail] = useState('');
const [enteredEmailTouched, setEnteredEmailTouched] = useState(false);
const enteredNameIsValid = enteredName.trim() !== '';
const nameInputIsInvalid = !enteredNameIsValid && enteredNameTouched;
const enteredEmailIsValid = enteredEmail.includes('@');
const enteredEmailIsInvalid = !enteredEmailIsValid && enteredEmailTouched;
let formIsValid = false;
if (enteredNameIsValid && enteredEmailIsValid) {
formIsValid = true;
}
const nameInputChangeHandler = (event) => {
setEnteredName(event.target.value);
};
const emailInputChangeHandler = (event) => {
setEnteredEmail(event.target.value);
};
const nameInputBlurHandler = (event) => {
setEnteredNameTouched(true);
};
const emailInputBlurHandler = (event) => {
setEnteredEmailTouched(true);
};
const formSubmissionHandler = (event) => {
event.preventDefault();
setEnteredNameTouched(true);
if (!enteredNameIsValid) {
return;
}
console.log(enteredName);
// nameInputRef.current.value = ''; => NOT IDEAL, DON'T MANIPULATE THE DOM
setEnteredName('');
setEnteredNameTouched(false);
setEnteredEmail('');
setEnteredEmailTouched(false);
};
const nameInputClasses = nameInputIsInvalid
? 'form-control invalid'
: 'form-control';
const emailInputClasses = enteredEmailIsInvalid
? 'form-control invalid'
: 'form-control';
return (
<form onSubmit={formSubmissionHandler}>
<div className={nameInputClasses}>
<label htmlFor='name'>Your Name</label>
<input
type='text'
id='name'
onChange={nameInputChangeHandler}
onBlur={nameInputBlurHandler}
value={enteredName}
/>
{nameInputIsInvalid && (
<p className='error-text'>Name must not be empty.</p>
)}
</div>
<div className={emailInputClasses}>
<label htmlFor='email'>Your E-Mail</label>
<input
type='email'
id='email'
onChange={emailInputChangeHandler}
onBlur={emailInputBlurHandler}
value={enteredEmail}
/>
{enteredEmailIsInvalid && (
<p className='error-text'>Please enter a valid email.</p>
)}
</div>
<div className='form-actions'>
<button disabled={!formIsValid}>Submit</button>
</div>
</form>
);
};
export default SimpleInput;
공통되는 부분을 Custom Hook으로 만든다.
import { useState } from 'react';
const useInput = (validateValue) => {
const [enteredValue, setEnteredValue] = useState('');
const [isTouched, setIsTouched] = useState(false);
const valueIsValid = validateValue(enteredValue);
const hasError = !valueIsValid && isTouched;
const valueChangeHandler = (event) => {
setEnteredValue(event.target.value);
};
const inputBlurHandler = (event) => {
setIsTouched(true);
};
const reset = () => {
setEnteredValue('');
setIsTouched(false);
};
return {
value: enteredValue,
isValid: valueIsValid,
hasError,
valueChangeHandler,
inputBlurHandler,
reset
};
};
export default useInput;
Custom Hook을 사용할 경우 더욱 간결하게 표현되는걸 확인할 수 있다.
import useInput from '../hooks/use-input';
const SimpleInput = (props) => {
const {
value: enteredName,
isValid: enteredNameIsValid,
hasError: nameInputHasError,
valueChangeHandler: nameChangedHandler,
inputBlurHandler: nameBlurHandler,
reset: resetNameInput,
} = useInput((value) => value.trim() !== '');
const {
value: enteredEmail,
isValid: enteredEmailIsValid,
hasError: emailInputHasError,
valueChangeHandler: emailChangeHandler,
inputBlurHandler: emailBlurHandler,
reset: resetEmailInput,
} = useInput((value) => value.includes('@'));
let formIsValid = false;
if (enteredNameIsValid && enteredEmailIsValid) {
formIsValid = true;
}
const formSubmissionHandler = (event) => {
event.preventDefault();
if (!enteredNameIsValid) {
return;
}
console.log(enteredName);
// nameInputRef.current.value = ''; => NOT IDEAL, DON'T MANIPULATE THE DOM
resetNameInput();
resetEmailInput();
};
const nameInputClasses = nameInputHasError
? 'form-control invalid'
: 'form-control';
const emailInputClasses = emailInputHasError
? 'form-control invalid'
: 'form-control';
return (
<form onSubmit={formSubmissionHandler}>
<div className={nameInputClasses}>
<label htmlFor='name'>Your Name</label>
<input
type='text'
id='name'
onChange={nameChangedHandler}
onBlur={nameBlurHandler}
value={enteredName}
/>
{nameInputHasError && (
<p className='error-text'>Name must not be empty.</p>
)}
</div>
<div className={emailInputClasses}>
<label htmlFor='email'>Your E-Mail</label>
<input
type='email'
id='email'
onChange={emailChangeHandler}
onBlur={emailBlurHandler}
value={enteredEmail}
/>
{emailInputHasError && (
<p className='error-text'>Please enter a valid email.</p>
)}
</div>
<div className='form-actions'>
<button disabled={!formIsValid}>Submit</button>
</div>
</form>
);
};
export default SimpleInput;
Custom Hook을 Reducer로 관리 할 경우
import { useReducer } from 'react';
const initialInputState = {
value: '',
isTouched: false,
};
const inputStateReducer = (state, action) => {
if (action.type === 'INPUT') {
return { value: action.value, isTouched: state.isTouched };
}
if (action.type === 'BLUR') {
return { isTouched: true, value: state.value };
}
if (action.type === 'RESET') {
return { isTouched: false, value: '' };
}
return inputStateReducer;
};
const useInput = (validateValue) => {
const [inputState, dispatch] = useReducer(
inputStateReducer,
initialInputState
);
const valueIsValid = validateValue(inputState.value);
const hasError = !valueIsValid && inputState.isTouched;
const valueChangeHandler = (event) => {
dispatch({ type: 'INPUT', value: event.target.value });
};
const inputBlurHandler = (event) => {
dispatch({ type: 'BLUR' });
};
const reset = () => {
dispatch({ type: 'RESET' });
};
return {
value: inputState.value,
isValid: valueIsValid,
hasError,
valueChangeHandler,
inputBlurHandler,
reset,
};
};
export default useInput;