> 폼의 유효성 검사
React Hook Form 의 중요 컨셉 중 하나는
register 를 통해 비제어 컴포넌트(uncontrolled component)를 Hook 과 연결하여 값이 검사될 수 있도록 만들고
폼을 제출할 때 한꺼번에 모아지도록 하는 것
각각의 필드는 등록 과정의 key 로 사용하기 위해 name 속성이 반드시 필요하다.
> useForm
export default function App() {
const { register, handleSubmit, errors } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form className="App" onSubmit={handleSubmit(onSubmit)}>
const onSubmit = (data) => { console.log(data); };
handleSubmit
보통 button 함수를 보면 e.preventDefault()
를 걸어놓는데 그 이유는
a 태그나 submit 타입을 가진 버튼의 경우 이벤트 이외에 각자의 고유한 기능을 가지고 있어서
우리가 선언한 이벤트 외에 페이지가 이동되는 것을 막기 위해서이다.
하지만 handleSubmit
을 이용하면 이를 해결가능하다.
register
ref = { register ( { ... } ) }
추가required
: 폼 제출을 위해 반드시 필요한 경우 truemin
max
minLength
maxLength
pattern
: 정규식 패턴validate
: 콜백 함수를 인수로 전달하거나errors
React Hook Form 은 폼 안의 에러를 보여주는 errors 객체를 제공
> 적용해보기
<label>First Name:</label> <input name="firstName" ref={register({ required: true })} /> {errors.firstName && <p>This is required</p>}
required: true
=> 필수 값
errors.firstName &&
=> firstName에 error가 있다면~~
필수 값 error 하나만 설정할때는 errors.lastName.type === "required"
생략해도 된다! (Last Name과 비교)
<label>Last Name:</label> <input name="lastName" ref={register({ required: true, minLength: 2 })} /> {errors.lastName && errors.lastName.type === "required" && ( <p>This is required</p> )} {errors.lastName && errors.lastName.type === "minLength" && ( <p>This is field required min lenght of 2</p> )}
required: true, minLength: 2
=> 필수 값 + 최소2글자
유효성 검사가 2개라 error문구도 각각 하나씩 작성한다.
errors.lastName.type === "required" && ( <p>This is required</p> )
errors.lastName.type === "minLength" && ( <p>This is field required min lenght of 2</p> )
아무것도 입력 안하면 => This is required
한글자만 입력하면 => This is field required min lenght of 2
<label>Age</label> <input name="age" type="number" ref={register({ required: true, min: 18, max: 99 })} /> {errors.age && errors.age.type === "required" && ( <p>Number is required</p> )} {errors.age && errors.age.type === ("min" || "max") && ( <p>Age is 18~99</p> )}
age 특성상 숫자여야해서 type="number"
를 추가하였다.
(typescript로 작성하면 훨씬 좋을 듯!! : number)
required: true, min: 18, max: 99
=>필수 값 + 18~99 숫자
errors.age.type === ("min" || "max") && ( <p>Age is 18~99</p> )
아무것도 입력 안하면 => This is required
한글자만 입력하면 => Age is 18~99
<label>Email</label> <input name="email" ref={register({ pattern: /^[\w.]+@[\w.]+\.[A-Za-z]{2,3}$/i` })} /> {errors.email && <p>Email is required</p>}
정규표현식 pattern: /^[\w.]+@[\w.]+\.[A-Za-z]{2,3}$/i
=> 이메일 형식 ( 한글은 불가)
아무것도 입력 안하면 => Email is required
이메일 형식 안 맞으면 => Email is required
<label>Gender</label> <select name="gender" ref={register({ required: true })}> <option value="">Select...</option> <option value="male">Male</option> <option value="female">Female</option> </select> {errors.gender && <p>This is required</p>}
<option value="">Select...</option>
를 설정> 사용자 정의 유효성 검사
register({ validate: ... }) / 비동기 함수
register의 속성 값인 validate의 사용법이다.
firstName
에 반드시 wooyoung
이 들어와야 하고
wooyoung
을 입력시 This is required
문구가 1초
뒤에 사라지게 만들었다.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const validateUserName = async (value) => { await sleep(1000); if (value === "wooyoung") return true; return false; }; ... <label>First Name:</label> <input name="firstName" ref={register({ required: true, validate: validateUserName })} /> {errors.firstName && <p>This is required</p>}
최소한의 기능을 구현해 보았는데
공식문서에는 추가적인 기능 및 구현 함수들이 정말 많이 있었다.
특히 저 내용들을 typescript로 작성해보는 연습은 시간을 내서 해봐야겠다.
> index.js
import React from "react";
import ReactDOM from "react-dom";
import useForm from "react-hook-form";
import "./styles.css";
function App() {
const { register, handleSubmit, errors } = useForm();
const onSubmit = (data) => {
console.log(data);
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const validateUserName = async (value) => {
await sleep(1000);
if (value === "wooyoung") return true;
return false;
};
// console.log(watch("firstName"));
return (
<form className="App" onSubmit={handleSubmit(onSubmit)}>
<h1>Sign Up</h1>
<label>First Name:</label>
<input
name="firstName"
ref={register({ required: true, validate: validateUserName })}
/>
{errors.firstName && <p>This is required</p>}
<label>Last Name:</label>
<input name="lastName" ref={register({ required: true, minLength: 2 })} />
{errors.lastName && errors.lastName.type === "required" && (
<p>This is required</p>
)}
{errors.lastName && errors.lastName.type === "minLength" && (
<p>This is field required min lenght of 2</p>
)}
<label>Age</label>
<input
name="age"
type="number"
ref={register({ required: true, min: 18, max: 99 })}
/>
{errors.age && errors.age.type === "required" && (
<p>Number is required</p>
)}
{errors.age && errors.age.type === ("min" || "max") && (
<p>Age is 18~99</p>
)}
<label>Email</label>
<input
name="email"
ref={register({ pattern: /^[\w.]+@[\w.]+\.[A-Za-z]{2,3}$/i })}
/>
{errors.email && <p>English is required</p>}
<label>Gender</label>
<select name="gender" ref={register({ required: true })}>
<option value="">Select...</option>
<option value="male">Male</option>
<option value="female">Female</option>
</select>
{errors.gender && <p>This is required</p>}
<input type="submit" />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
> styles.css
body {
background: #0e101c;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
}
form {
max-width: 500px;
margin: 0 auto;
}
h1 {
font-weight: 100;
color: white;
text-align: center;
padding-bottom: 10px;
border-bottom: 1px solid rgb(79, 98, 148);
}
.form {
background: #0e101c;
max-width: 400px;
margin: 0 auto;
}
p {
color: #bf1650;
}
p::before {
display: inline;
content: "⚠ ";
}
input {
display: block;
box-sizing: border-box;
width: 100%;
border-radius: 4px;
border: 1px solid white;
padding: 10px 15px;
margin-bottom: 10px;
font-size: 14px;
}
label {
line-height: 2;
text-align: left;
display: block;
margin-bottom: 13px;
margin-top: 20px;
color: white;
font-size: 14px;
font-weight: 200;
}
button[type="submit"],
input[type="submit"] {
background: #ec5990;
color: white;
text-transform: uppercase;
border: none;
margin-top: 40px;
padding: 20px;
font-size: 16px;
font-weight: 100;
letter-spacing: 10px;
}
button[type="submit"]:hover,
input[type="submit"]:hover {
background: #bf1650;
}
button[type="submit"]:active,
input[type="button"]:active,
input[type="submit"]:active {
transition: 0.3s all;
transform: translateY(3px);
border: 1px solid transparent;
opacity: 0.8;
}
input:disabled {
opacity: 0.4;
}
input[type="button"]:hover {
transition: 0.3s all;
}
button[type="submit"],
input[type="button"],
input[type="submit"] {
-webkit-appearance: none;
}
.App {
max-width: 600px;
margin: 0 auto;
}
button[type="button"] {
display: block;
appearance: none;
background: #333;
color: white;
border: none;
text-transform: uppercase;
padding: 10px 20px;
border-radius: 4px;
}
hr {
margin-top: 30px;
}
button {
display: block;
appearance: none;
margin-top: 40px;
border: 1px solid #333;
margin-bottom: 20px;
text-transform: uppercase;
padding: 10px 20px;
border-radius: 4px;
}