추석기간동안 과제로 BE, FE 를 합친 todo 앱 만들기가 주어졌다.
때마침 리액트 스터디에서 redux 로 todo 앱을 만들었던 게 있어서 해당 프로젝트에 BE 와 react-hook-form 을 추가하는 식으로 코드를 짜봤다.
이번 과제를 진행하며 아래의 사항에 대해 새로이 알 수 있었다.
결과 미리보기
할일 추가, 삭제, 수정, 전체 조회 기능의 REST API 를 붙였고 그 외에 필터 기능을 추가하여 모든 할일 체크하기와 완료한 일 삭제하기, 필터기능 적용하기 등을 만들었다.
그러기 위해 먼저 BE 코드가 준비되어야 했다.
BE 의 요구사항은 다음과 같다.
Get /todos
, POST /todo
, PATCH /todo/:todoId
, DELETE /todo/todoId
가 필요햐다.sequelize 도 사용해도 좋으나 구조가 todo 테이블 하나밖에 없어서 그냥 mvc 패턴도 쓰지 않고 Node.js express 에 MySQL 만 사용하는 식으로 코드를 구성했다.
├── server
│ ├── controller
│ │ └── Cmain.js
│ ├── index.js
│ ├── model
│ │ └── Model.js
│ ├── package-lock.json
│ ├── package.json
│ ├── routes
│ │ └── main.js
컨트롤러, 모델을 각각 하나씩 사용했고 view 에 ejs 를 사용할 필요 없이 React 에서 화면을 띄울 것이라 controller 에서는 데이터만 전달하려고 res.json 을 사용했다.
// server/controller/Cmain.js
const model = require("../model/Model");
const getTodos = (req, res) => {
model.db_getTodos(undefined, (result) => {
res.json(result);
});
};
const postTodo = (req, res) => {
model.db_postTodo(req.body, () => {
res.json({ result: true });
});
};
const patchTodo = (req, res) => {
const id = req.params.todoId;
const { title, done } = req.body;
model.db_update({ id, title, done }, () => {
res.json({ result: true });
});
};
const deleteTodo = (req, res) => {
const id = req.params.todoId;
model.db_delete({ id }, () => {
res.json({ result: true });
});
};
module.exports = {
getTodos,
postTodo,
patchTodo,
deleteTodo,
};
// server/model/Model.js
const mysql = require("mysql");
require('dotenv').config()
//mysql연결
const conn = mysql.createConnection({
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DB,
port: process.env.MYSQL_PORT,
});
conn.connect(function (err) {
if (err) throw err;
console.log("connected!");
});
const db_getTodos = (data, cb) => {
const query = "select * from todo";
conn.query(query, (err, result) => {
if (err) {
console.error(err);
return;
}
cb(result);
});
};
const db_postTodo = (data, cb) => {
const query = "insert into todo (title, done) values (?, ?)";
conn.query(query, [data.title, data.done], (err, result) => {
if (err) {
console.error(err);
return;
}
cb();
});
};
const db_update = (data, cb) => {
const query = "update todo set title = ?, done= ? where id = ?";
conn.query(query, [data.title, data.done, data.id], (err, result) => {
if (err) {
console.error(err);
return;
}
cb();
});
};
const db_delete = (data, cb) => {
const query = "delete from todo where id = ?";
conn.query(query, [data.id], (err, result) => {
if (err) {
console.error(err);
return;
}
cb();
});
};
module.exports = {
db_getTodos,
db_postTodo,
db_update,
db_delete,
};
// server/routes/main.js
const express = require("express");
const router = express.Router();
const controller = require("../controller/Cmain");
router.get("/todos", controller.getTodos);
router.post("/todo", controller.postTodo);
router.patch("/todo/:todoId", controller.patchTodo);
router.delete("/todo/:todoId", controller.deleteTodo);
module.exports = router;
서버의 index.js 엔트리보인트에 혹시몰라 cors 설정을 위한 코드를 추가해두었다.
// server/index.js
const express = require("express");
const app = express();
const PORT = 8000;
const cors = require("cors");
app.use(cors());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
//router 분리
const router = require("./routes/main");
app.use("/", router);
//오류처리
app.use("*", (req, res) => {
res.status(404).render("404");
});
app.listen(PORT, () => {
console.log(`http://localhost:${PORT}`);
});
각 코드를 짜는동안 postman 으로 동작 여부를 체크했다.
db 와 테이블 자체는 workbench 에서 직접 생성했다.
create database mini;
use mini;
create table todo (
id int not null primary key auto_increment,
title varchar(100) not null,
done tinyint(1) not null default 0
);
select * from todo;
기왕 redux 를 사용하려고 한거 react-hook-form 을 활용하고자 했다.
기존에 redux 로 todo 앱을 만들었을 때는 DB 를 연결해두지 않아서 slice 에서 reducer 를 만들어 사용했는데 db 를 연동하고자 하니 axios 로 api 를 호출해야 하기도 하고 비동기 처리를 위해 createAsyncThunk 라는 메서드를 사용해야 했다.
먼저 redux 를 어떻게 활용했는지 정리해보자.
프론트엔드 클라이언트 구조는 다음과 같다.
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── app
│ │ └── store.js
│ ├── features
│ │ ├── filters
│ │ │ ├── Filter.jsx
│ │ │ └── filtersSlice.js
│ │ └── todos
│ │ ├── Todo.jsx
│ │ └── todosSlice.js
비동기가 아니라면 reducer, action 등을 redux/toolkit 의 createSlice 를 통해 한번에 생성이 가능했는데 API 를 연결해야 하다보니 비동기 처리가 필요하여 createAsyncThunk
를 사용해야 했다.
redux/tookit 의 메서드로 redux action type 문자열
과 promise 를 반환하는 콜백 함수
를 갖는 함수를 말한다.
이 함수는 전달한 액션 타입에 기반한 promise 생명주기의 액션을 생성하고 promise callback 을 실행할 thunk action 생성자를 반환한다. 그리고 반환된 promise 에 기반한 생명주기 action 을 dispatch 한다.
즉,
createAsyncThunk
는 async 요청을 다루는 과정을 표준의 권장된 방식으로 추상화한다.
이 함수는 action.payload 처럼 어떤 데이터를 fetching 할 것인지, 어떻게 loading stating 를 추적하길 원하는지 혹은 반환될 데이터가 어떻게 진행될지 알 수 없기에 어떤 리듀서 함수도 생성하지 않는다.
따라서 createAsyncThunk
는 비동기 로직을 수행하거나 그 결과 특정 데이터를 생성해주기만 하고 그 이후 해당 데이터 및 로직 수행의 결과를 어디서 가져다 사용하고 store 의 어떤 state 에 저장하고 활용할지는 직접 처리해줘야 한다.
다음은 Redux Toolkit docs 에서 소개하는 예시코드다.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
interface UsersState {
entities: []
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}
const initialState = {
entities: [],
loading: 'idle',
} as UsersState
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
createAsyncTHunk
함수의 결과를 활용하는 리듀서 동작을 추가하기 위해 extraReducer 에 addCase 를 통해 state 관리를 하고 있음을 알 수 있다.
createAsyncThunk
는 3가지 파라미터를 가질 수 있다.
- action
type
value
- action type value 에
users/requestStatus
를 전달했다면 이는 다음 3가지 액션 타입을 생성해준다.
pending
:users/requestStatus/pending
fulfilled
:users/requestStatus/fulfilled
rejected
:users/requestStatus/rejected
preloadCreator
callback
- 비동기 로직의 결과를 포함하는 promise 를 반환해야 하는 콜백함수이다. 이 함수는 값을 동기적으로 반환할수도 있다.
- 만약 콜백함수를 실행하는데 오류가 발생한다면 Error 인스턴스를 포함하는 rejected 프로미스를 반환할 것이다.
preloadCreator
는 2개의 인자를 갖고 호출할 수 있다.
arg
: 하나의 값을 전달해야 하는데 여러 값을 전달하기 원할시 객체에 담아 전달한다.thunkAPI
:Redux thunk function
으로 전달되는 파라미터들을 포함하는 객체
dispatch
: store 의dispatch
메서드getState
: store 의getState
메서드
...
options
:condition, dispatchConditionRejection, idGenerator(arg)
등의 선택 필드들을 갖는 객체인데 실제로 사용해보지는 않았다.
실제로 적용해본 코드
export const editTodo = createAsyncThunk(
"todos/editTodo",
async ({ id, newText, completed }, { dispatch, getState }) => {
const currentState = getState();
const isDone = currentState.todos.todos.find(
(todo) => todo.id === id
).completed;
try {
const response = await axios.patch(
`http://localhost:8000/todo/${id + 1}`,
{
title: newText,
done: isDone ? 1 : 0,
}
);
if (response.data.result) {
dispatch(
todosSlice.actions.editTodoState({
id,
text: newText,
completed,
})
);
}
} catch (error) {
console.error("Error editing todo:", error);
throw error;
}
}
);
createAsyncTHunk
가 Redux thunk action creator
를 반환하기 때문에
dispatch(editTodo)
dispatch(editTodo.pending)
dispatch(editTodo.fulfilled)
dispatch(editTodo.rejected)
같이 코드를 사용할 수 있다.
import { createAsyncThunk } from '@reduxjs/toolkit';
import { useDispatch } from 'react-redux';
// Define an asynchronous thunk action
export const fetchUserById = createAsyncThunk(
'user/fetchById',
async (userId, thunkAPI) => {
try {
// Simulate an API call or any asynchronous operation
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
const userData = await response.json();
return userData;
} catch (error) {
throw error;
}
}
);
// Example usage in a React component
const MyComponent = () => {
const dispatch = useDispatch();
const userId = 123; // Replace with the desired user ID
const fetchData = async () => {
try {
// Dispatch the pending action
dispatch(fetchUserById.pending());
// Dispatch the asynchronous operation
const resultAction = await dispatch(fetchUserById(userId));
// Check if the operation was fulfilled
if (fetchUserById.fulfilled.match(resultAction)) {
// Dispatch the fulfilled action
dispatch(fetchUserById.fulfilled(resultAction.payload));
} else if (fetchUserById.rejected.match(resultAction)) {
// Dispatch the rejected action
dispatch(fetchUserById.rejected(resultAction.error));
}
} catch (error) {
console.error('Error:', error);
}
};
return (
<div>
<button onClick={fetchData}>Fetch User</button>
</div>
);
};
export default MyComponent;
createAsyncThunk
는 pending
, fulfilled
, rejected
같은createAction
를 사용하는 3가지 Redux action createor 를 생성한다.
이런 각각의 생명주기 액션 생성자는 dispatche 될때 reducer 로직이 액션 타입을 참조하고 액션에 응답할 수 있도록 반환된 액션 생성자에 첨부될 것이다.
각 액션 객체는 action.meta
아래 현재의 유일한 requestId
와 arg
값을 포함할 것이다.
실제 코드에서 사용해본 예시
extraReducers: (builder) => {
builder
.addCase(fetchTodos.fulfilled, (state, action) => {
state.todos = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
console.error("Error fetching todos:", action.error);
});
},
각강의 생명주기 액션 생성자 인터페이스 구조
interface PendingAction<ThunkArg> {
type: string
payload: undefined
meta: {
requestId: string
arg: ThunkArg
}
}
interface FulfilledAction<ThunkArg, PromiseResult> {
type: string
payload: PromiseResult
meta: {
requestId: string
arg: ThunkArg
}
}
interface RejectedAction<ThunkArg> {
type: string
payload: undefined
error: SerializedError | any
meta: {
requestId: string
arg: ThunkArg
aborted: boolean
condition: boolean
}
}
이런 액션들을 리듀서에서 다루기 위해 키 노테이션 객체나 builder callback
노테이션을 사용하여 createReducer
혹은 createSlice
에서 액션 생성자를 참조하자.
const reducer1 = createReducer(initialState, {
[fetchUserById.fulfilled]: (state, action) => {},
})
const reducer2 = createReducer(initialState, (builder) => {
builder.addCase(fetchUserById.fulfilled, (state, action) => {})
})
const reducer3 = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: {
[fetchUserById.fulfilled]: (state, action) => {},
},
})
const reducer4 = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUserById.fulfilled, (state, action) => {})
},
})
input 이 들어간 부분에서 보통 react 는 다음과 같은 구조로 코드를 작성한다.
export defualt function Todo() {
const [ inputText, setInputText ] = useState("");
return (<>
<input type="text" value={inputText} onChange={(e) => setInputText(e.target.value)} />
<button type="button" onClick={() => {dispatch(todoAdd(inputText))}}>
</>)
}
대강 작성하긴 해쓴데 input 에 들어갈 값을 매 순간 useState 로 만든 state 와 setState 를 통해 업데이트하기에 input 박스에 글자 하나를 입력할 때마다 상태가 변경되어 매번 재렌더링이 된다.
그런데 react-hook-form 을 사용하면 submit 이 될때까지 재렌더링이 되지 않는다. 심지어 그와 관련된 커스텀 메서드들을 제공하고 있어 form 을 작성하는 경우 매우 유용해 보였다.
예시 코드
import { useForm } from "react-hook-form";
export default function Form2() {
const {
register,
handleSubmit,
formState: { errors },
watch,
} = useForm();
// handleSubmit : 두개의 함수를 받는데 하나는 유효할때 실행되는 함수, 하나는 오류가 발생할때 실행되는 함수이다.
const onValid = (data) => {
console.log("data : ", data);
};
return (
<>
<form onSubmit={handleSubmit(onValid)}>
<input
placeholder="이름"
type="text"
{...register("name", {
required: "이름은 필수 항목입니다.",
})}
/>
{errors.name?.message}
<input
placeholder="나이"
type="text"
{...register("age", {
required: "나이를 입력하세요",
min: { message: "0 이상의 숫자만 입력 가능합니다.", value: 0 },
})}
/>
{errors.age?.message}
<button type="submit">제출</button>
</form>
</>
);
}
todo 앱에서는 input 을 두군데 사용했다.
할일 수정의 경우 체크 여부, 할일 텍스트 수정, 삭제 버튼 등이 엮여있어서 할일 추가에만 react-hook-form 을 적용했다.
react-hook-form 적용해본 코드
import { useForm } from "react-hook-form";
import { todoAdded } from "./todosSlice";
export default function TodoForm({ dispatch }) {
const { register, handleSubmit, reset } = useForm();
const onSubmit = (data) => {
dispatch(todoAdded(data.textInput));
reset();
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
style={{
display: "flex",
justifyContent: "space-between",
margin: "10px",
width: "100%",
}}
>
<input
type="text"
style={{ width: "90vw" }}
placeholder="할일을 추가하세요"
{...register("textInput", { required: true })}
/>
<button type="submit">add</button>
</form>
);
}
react hook form 에 대해 알아보자.
설치하기
npm install react-hook-form
예시코드
import { useForm } from "react-hook-form"
export default function App() {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm()
const onSubmit = (data) => console.log(data)
console.log(watch("example")) // watch input value by passing the name of it
return (
/* "handleSubmit" will validate your inputs before invoking "onSubmit" */
<form onSubmit={handleSubmit(onSubmit)}>
{/* register your input into the hook by invoking the "register" function */}
<input defaultValue="test" {...register("example")} />
{/* include validation with required or other standard HTML validation rules */}
<input {...register("exampleRequired", { required: true })} />
{/* errors will return when field validation fails */}
{errors.exampleRequired && <span>This field is required</span>}
<input type="submit" />
</form>
)
}
react-hook-form 의 useForm() 메서드를 통해 여러가지 메서드를 얻을 수 있는데 그중 register 를 통해 각 input 에 대해 얻을 data 가 객체 내에 어떤 key 로 들어오고, validation 중 무엇을 확인할지 등을 정할 수 있다.
import { useForm } from "react-hook-form";
import { todoAdded } from "./todosSlice";
export default function TodoForm({ dispatch }) {
const { register, handleSubmit, reset } = useForm();
const onSubmit = (data) => {
console.log("data : ", data);
dispatch(todoAdded(data.textInput));
reset();
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
style={{
display: "flex",
justifyContent: "space-between",
margin: "10px",
width: "100%",
}}
>
<input
type="text"
style={{ width: "90vw" }}
placeholder="할일을 추가하세요"
{...register("textInput", { required: true })}
/>
<button type="submit">add</button>
</form>
);
}
예를 들어 앞에서 소개했던 코드를 다시 가져와보면 register 에 첫번째 인자로 전달한 문자열이 handleSubmit 의 인자로 전달한 onSubmit 메서드의 인자가 갖는 객체의 key 가 된다.
정확하게는 onSubmit 의 인자를 data 라고 했는데 해당 data 는 브라우저 콘솔기록을 보면 알 수 있듯이 { textInput: "" }
이런 형태를 갖는 객체이다.
그래서 form 에서 input 에 값을 이름지어서 붙여주고 유효성를 판단할때 register 와 handleSubmit 등을 사용할 수 있다.
register 가 유효성을 평가할 수 있는 사항은 다음과 같다.
import { useForm } from "react-hook-form"
export default function App() {
const { register, handleSubmit } = useForm()
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { required: true, maxLength: 20 })} />
<input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
<input type="number" {...register("age", { min: 18, max: 99 })} />
<input type="submit" />
</form>
)
}
만약 Material UI 나 AntD 같이 외부 UI 라이브러리와 결합하려면 react-hook-form 의 Controller 로 감싸서 사용할 수 있다.
import Select from "react-select"
import { useForm, Controller } from "react-hook-form"
import Input from "@material-ui/core/Input"
const App = () => {
const { control, handleSubmit } = useForm({
defaultValues: {
firstName: "",
select: {},
},
})
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="firstName"
control={control}
render={({ field }) => <Input {...field} />}
/>
<Controller
name="select"
control={control}
render={({ field }) => (
<Select
{...field}
options={[
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" },
]}
/>
)}
/>
<input type="submit" />
</form>
)
}
react-redux
의 경우 connect 를 통해 react-hook-form
과 같이 사용할 수 있다.
import { useForm } from "react-hook-form"
import { connect } from "react-redux"
import updateAction from "./actions"
export default function App(props) {
const { register, handleSubmit, setValue } = useForm({
defaultValues: {
firstName: "",
lastName: "",
},
})
// Submit your data into Redux store
const onSubmit = (data) => props.updateAction(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input type="submit" />
</form>
)
}
// Connect your component with redux
connect(
({ firstName, lastName }) => ({ firstName, lastName }),
updateAction
)(YourForm)
그러나 redux 공식문서에 가보면 connect 는 좀 이전 버전의 메서드인 것 같다. 여전히 8.x 버전에서 지원은 하지만 useDispatch(), useSelector() 등의 hook API 를 사용하는 것을 권장하고 있었다.
음... 위의 코드는 react-hook-form 에서 get start 에 소개되고 있는 코드인데 업데이트를 안한지 좀 된건가? react redux 에서는 하지 말라는 형태의 코드 모양을 보여주고 있다.
react-redux 의
connect()
함수는 React 컴포넌트를 Redux 스토어로 연결시켜준다.
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
mapStateToProps
: redux store 의 state
를 다루는 함수const mapStateToProps = (state) => ({ todos: state.todos })
///
const mapStateToProps = (state, ownProps) => ({
todo: state.todos[ownProps.id],
})
mapDispatchToProps
: redux store 의 dispatch
를 다루는 함수 혹은 객체const mapDispatchToProps = (dispatch) => {
return {
// dispatching plain actions
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
reset: () => dispatch({ type: 'RESET' }),
}
}
///
// binds on component re-rendering
<button onClick={() => this.props.toggleTodo(this.props.todoId)} />
// binds on `props` change
const mapDispatchToProps = (dispatch, ownProps) => ({
toggleTodo: () => dispatch(toggleTodo(ownProps.todoId)),
})
useForm() 에 formState: { errors }
를 통해 에러를 관리할 수 있다.
import { useForm } from "react-hook-form"
export default function App() {
const {
register,
formState: { errors },
handleSubmit,
} = useForm()
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("firstName", { required: true })}
aria-invalid={errors.firstName ? "true" : "false"}
/>
{errors.firstName?.type === "required" && (
<p role="alert">First name is required</p>
)}
<input
{...register("mail", { required: "Email Address is required" })}
aria-invalid={errors.mail ? "true" : "false"}
/>
{errors.mail && <p role="alert">{errors.mail.message}</p>}
<input type="submit" />
</form>
)
}
6개의 대표적인 hook 이 있음을 알 수 있다. 아직 useForm 만 사용해봤는데 한번 알아보자.
이 페이지에서는 useForm
이 최소한의 재렌더링으로 form 의 유효성을 확인하는 강력한 커스텀 훅이라고 소개하고 있다.
useForm 함수가 반환하는 객체가 위와 같은 메서드들을 포함하고 있다.
register 메서드는 input 혹은 select 요소노드를 등록할 수 있게 해주고 React Hook Form 의 규칙에 따라 유효성 검사를 실시한다.
- 유효성 검사는 HTML 표준에 따른 것이고 커스텀 유효성 검사도 실행할 수 있다.
register 가 기본적으로 onCHange, onBlur, ref, name 을 props 로 갖고 있기에 여러 props 를 명시할 필요 없이 ...regiser 로 이름만 전달해주면 된다.
const { onChange, onBlur, name, ref } = register('firstName');
// include type check against field path with the name you have supplied.
<input
onChange={onChange} // assign onChange event
onBlur={onBlur} // assign onBlur event
name={name} // assign name prop
ref={ref} // assign ref prop
/>
// same as above
<input {...register('firstName')} />
구체적인 예시코드는 register 를 활용할때 docs 를 참고하자. 예시코드가 정말 세세하게 잘 제공되어 있다.
그중 두가지정도 예시를 기록해두자면 에러가 발생시 표시할 메세지를 지정할 수 있고 onChange, onBlur 등이 어떻게 동작할지도 설정할 수 있다.
<input
type="number"
{...register("test", {
min: {
value: 3,
message: 'error message' // JS only: <p>error message</p> TS only support string
}
})}
/>
///
register('firstName', {
onChange: (e) => console.log(e)
})
주의!
- 첫번째 인자로 전달하는 name 은 필수적으로 전달해야하고 unique 해야 한다.
이 객체는 전체 form 상태의 정보를 갖고 있다. 이는 form 어플리케이션에서 유저와의 상호작용을 유지하고 추적할 수 있도록 도와준다.
isDirty
- 사용자가 입력값을 수정하면 true, 기본값이면 false
const {
formState: { isDirty, dirtyFields },
setValue,
} = useForm({ defaultValues: { test: "" } });
// isDirty: true
setValue('test', 'change')
// isDirty: false because there getValues() === defaultValues
setValue('test', '')
touchedFields
- 사용자가 상호작용한 모든 input 을 갖는 객체
defaultValues
- useForm 의 defaultValue 혹은 reset API 를 통해 업데이트된 defaultValue 를 통해 설정된 값 객체
isSubmitted
- form 이 제출되었으면 true, reset 메서드가 실행되기까지 true 로 유지된다.
isLoading
- form 이 현재 비동기 기본값을 로딩중이면 true
- 오직 async defaultValue 에만 사용할 수 있다.
const {
formState: { isLoading }
} = useForm({
defaultValues: async () => await fetch('/api')
});
submitCount
- form 이 submitted 한 횟수
isValid
- 만약 form 이 아무 에러도 갖지 않는다면 true
errors
- 에러 필드들을 갖는 객체
실습때 이 errors 를 활용하여 validation 을 위반할 때의 message 를 통해 form 에 어떤 점에서 유효성에 위배되는지 나타내줄 수 있었다.
import { useForm } from "react-hook-form";
export default function Form2() {
const {
register,
handleSubmit,
formState: { errors },
watch,
} = useForm();
return (
<>
<form onSubmit={handleSubmit()}>
<input
placeholder="나이"
type="text"
{...register("age", {
required: "나이를 입력하세요",
min: { message: "0 이상의 숫자만 입력 가능합니다.", value: 0 },
})}
/>
{errors.age?.message}
<button type="submit">제출</button>
</form>
</>
);
}
이 메서드는 구체적인 입력을 계속 지켜보고 그들의 값을 반환한다. 이 메서드는 입력 값을 렌더링하고 조건에 따라 무엇을 렌더링할지 결정할 때 사용된다.
인자로 아무것도 전달하지 않으면 모든 입력을 watch 하고 ( watch 를 뭐라고 표현해야 할지 모르겠다.. ) 특정 문자열이나 배열을 전달시 해당 이름을 갖는 입력들만 watch 할 수 있다. 또한 콜백도 따로 지정이 가능한 것 같다.
react hook form docs 는 예시코드가 상세히 나와있어서 너무 좋다..
import React from "react"
import { useForm } from "react-hook-form"
function App() {
const {
register,
watch,
formState: { errors },
handleSubmit,
} = useForm()
const watchShowAge = watch("showAge", false) // you can supply default value as second argument
const watchAllFields = watch() // when pass nothing as argument, you are watching everything
const watchFields = watch(["showAge", "number"]) // you can also target specific fields by their names
// Callback version of watch. It's your responsibility to unsubscribe when done.
React.useEffect(() => {
const subscription = watch((value, { name, type }) =>
console.log(value, name, type)
)
return () => subscription.unsubscribe()
}, [watch])
const onSubmit = (data) => console.log(data)
return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<input type="checkbox" {...register("showAge")} />
{/* based on yes selection to display Age Input*/}
{watchShowAge && (
<input type="number" {...register("age", { min: 50 })} />
)}
<input type="submit" />
</form>
</>
)
}
이 함수는 만약 form 의 유효석 검사가 성공적이었다면 form 데이터를 받는다.
handleSubmit(onSubmit)()
// You can pass an async function for asynchronous validation.
handleSubmit(async (data) => await fetchAPI(data))
props 를 보니 form 이 성공적으로 제출되면 data 를, 아니라면 errors 를 받는 것 같다.
sync
import { useForm } from "react-hook-form"
export default function App() {
const { register, handleSubmit } = useForm()
const onSubmit = (data, e) => console.log(data, e)
const onError = (errors, e) => console.log(errors, e)
return (
<form onSubmit={handleSubmit(onSubmit, onError)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<button type="submit">Submit</button>
</form>
)
}
async
import React from "react";
import { useForm } from "react-hook-form";
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
function App() {
const { register, handleSubmit, formState: { errors }, formState } = useForm();
const onSubmit = async data => {
await sleep(2000);
if (data.username === "bill") {
alert(JSON.stringify(data));
} else {
alert("There is an error");
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="username">User Name</label>
<input placeholder="Bill" {...register("username"} />
<input type="submit" />
</form>
);
}
reset 은 form 상태, field reference, subscription 등을 전부 초기화한다. 선택적인 인자를 전달해서 form state 일부만 리셋할수도 있다.
제출 이후 useEffect 내부에서 reset 하는 것을 권장하고 있다.
useEffect(() => {
reset({
data: "test",
})
}, [isSubmitSuccessful])
import React, { useCallback } from "react"
import { useForm } from "react-hook-form"
export default function App() {
const { register, handleSubmit, reset } = useForm()
const resetAsyncForm = useCallback(async () => {
const result = await fetch("./api/formValues.json") // result: { firstName: 'test', lastName: 'test2' }
reset(result) // asynchronously reset your form values
}, [reset])
useEffect(() => {
resetAsyncForm()
}, [resetAsyncForm])
return (
<form onSubmit={handleSubmit((data) => {})}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input
type="button"
onClick={() => {
reset(
{
firstName: "bill",
},
{
keepErrors: true,
keepDirty: true,
}
)
}}
/>
<button
onClick={() => {
reset((formValues) => ({
...formValues,
lastName: "test",
}))
}}
>
Reset partial
</button>
</form>
)
}
reset 은 사용할 경우가 자주 있을것 같아서 docs 주소를 남긴다. 추후 코드 작성시 예시코드를 참고하자.
이 함수는 하나 혹은 그 이상의 오류를 수동으로 설정할 수 있게 해준다.
import { useForm } from "react-hook-form"
const App = () => {
const {
register,
setError,
formState: { errors },
} = useForm()
return (
<form>
<input {...register("test")} />
{errors.test && <p>{errors.test.message}</p>}
<button
type="button"
onClick={() => {
setError("test", { type: "focus" }, { shouldFocus: true })
}}
>
Set Error Focus
</button>
<input type="submit" />
</form>
)
}
form 값을 읽는데 최적화된 헬퍼이다. watch 와 getValue 의 차이점으로 getValue 는 재렌더링을 촉발하거나 입력의 변화에 subscribe 를 하고 있지 않다는 점이 있다.
그래서 예시코드에서도 getValues 가 지속적으로 변화사항을 추적하고 있지 않으니 onClick 에서 실행하도록 하고 있는 것 같다.
import { useForm } from "react-hook-form"
export default function App() {
const { register, getValues } = useForm()
return (
<form>
<input {...register("test")} />
<input {...register("test1")} />
<button
type="button"
onClick={() => {
const values = getValues() // { test: "test-input", test1: "test1-input" }
const singleValue = getValues("test") // "test-input"
const multipleValues = getValues(["test", "test1"])
// ["test-input", "test1-input"]
}}
>
Get Values
</button>
</form>
)
}
수동으로 form 혹은 입력에 대해 유효성 검사를 실행하도록 할 수 있다. 이 메서드는 임력 유효성 검사가 다른 입력값에 의존적인 dependant validation 을 해야할때 유용하다.
import React from "react"
import { useForm } from "react-hook-form"
export default function App() {
const {
register,
trigger,
formState: { errors },
} = useForm()
return (
<form>
<input {...register("firstName", { required: true })} />
<input {...register("lastName", { required: true })} />
<button
type="button"
onClick={async () => {
const result = await trigger("lastName")
// const result = await trigger("lastName", { shouldFocus: true }); allowed to focus input
}}
>
Trigger
</button>
<button
type="button"
onClick={async () => {
const result = await trigger(["firstName", "lastName"])
}}
>
Trigger Multiple
</button>
<button
type="button"
onClick={() => {
trigger()
}}
>
Trigger All
</button>
</form>
)
}
이 객체는 React Hook Form 으로 컴포넌트를 등록하기 위한 메서드를 포함하고 있다.
그러나 이 객체에 직접적으로 접근해서는 안되며 control은 내부적으로 사용하기 위한 객체이다.
import { useForm, Controller } from "react-hook-form"
function App() {
const { control } = useForm()
return (
<Controller
render={({ field }) => <input {...field} />}
name="firstName"
control={control}
defaultValue=""
/>
)
}