여러 개의 하위 값을 포함하는 복잡한 state를 다뤄야 할 때 사용합니다.
컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있습니다.
const reducer = (prevState, action) => {
prevState // initialState
action.type // "타입이름"
action.payload // 전달값
return 새 state가 될 값
};
const [state, dispatch] = useReducer(reducer, initialState, initFn);
// dispatch(action)
dispatch({type: "타입이름", payload: 전달값});
reducer이전 state와 action을 받아 return을 통해 state를 업데이트 시켜주는 역할을 합니다.
useReducer첫째 파라미터는 reducer 함수이고, 둘째 파라미터는 초기 state입니다.
반환하는 배열의 첫째 값은 업데이트 된 state이며, 둘째 값은 dispatch 함수 입니다.
dispatch액션을 발생시켜, dispatch내부 action객체에 값을 담아 reducer에게 전달합니다.
actionreducer에게 전달할 업데이트를 위한 값을 가지고 있습니다.
type과 payload를 가지고 있습니다.
typeaction을 구분을 위한 데이터를 가지고 있습니다.
payload전달할 값을 가지고 있습니다.
import React, { useEffect, useState } from "react";
import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";
const Login = (props) => {
const [enteredEmail, setEnteredEmail] = useState("");
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState("");
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
// input에 키를 입력하면 유효성 검사
useEffect(() => {
const identifier = setTimeout(() => {
setFormIsValid(
enteredEmail.includes("@") && enteredPassword.trim().length > 6
);
}, 300);
return () => {
clearTimeout(identifier);
};
}, [enteredEmail, enteredPassword]);
// input에 키를 입력하면 enteredEmail, enteredPassword state 변경
const emailChangeHandler = (event) => {
setEnteredEmail(event.target.value);
};
const passwordChangeHandler = (event) => {
setEnteredPassword(event.target.value);
};
// input 포커스를 해제하면 emailIsValid, passwordIsValid state 변경
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes("@"));
};
const validatePasswordHandler = () => {
setPasswordIsValid(enteredPassword.trim().length > 6);
};
// form 제출하면 윗 컴포넌트로 enteredEmail, enteredPassword state 전달
const submitHandler = (event) => {
event.preventDefault();
props.onLogin(enteredEmail, enteredPassword);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={enteredEmail}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div
className={`${classes.control} ${
passwordIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={enteredPassword}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
</div>
<div className={classes.actions}>
/* 유효성 검사 통과되어야 Button 활성화 */
<Button type="submit" className={classes.btn} disabled={!formIsValid}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
const [enteredEmail, setEnteredEmail] = useState("");
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState("");
const [passwordIsValid, setPasswordIsValid] = useState();
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes("@"));
};
const validatePasswordHandler = () => {
setPasswordIsValid(enteredPassword.trim().length > 6);
};
setEmailIsValid()와 setPasswordIsValid()는 각각 emailIsValid, passwordIsValid state를 변화하는 함수입니다.
하지만 위의 코드에서는 IsValid state가 아닌enteredEmail과 enteredEmail state를 바꾸는 역할을 하고 있습니다.
이런 경우에 다음 렌더링에 변화될 예약만 되고 최신으로 업데이트되지 않은 enteredEmail을 기준으로 emailIsValid 업데이트가 될 수 있습니다.
따라서 기존 state를 기준으로 바꿀 수 있도록 setEmailIsValid를 함수 폼(기존state => 기존state를 기준으로 변화)으로 사용해야 하는데 setEmailIsValid은 이전 emailIsValid만 사용할 수 있습니다.
이런 경우에 값과 유효성을 하나의 객체로 사용하는 state로 관리할 수 있습니다.
하지만 이렇게 state가 복잡하고 커지게 된다면 useReducer을 사용하는 것이 좋을 수 있습니다.
reducer 함수 내부에서는 컴포넌트 함수 내부에서 만들어진 어떤 데이터도 필요하지 않기 때문입니다.
리듀서 함수는 이 컴포넌트 함수의 범위 밖에서 사용할 수 있습니다.
action의 type이 "USER_INPUT"일 경우 input 값이 변경되고 있는 것이기 때문에 state를 action의 payload로 업데이트 시켜 반환합니다
useReducer에서 유효성 state와 input value state를 한번에 관리합니다.
useEffect의 의존성 배열에는 유효성 state만 포함시킵니다.
유효성은 충족되어 더이상 useEffect를 재실행 하지 않아도 되지만 value가 변해서 불필요하게 useEffect를 재실행 할 수 있기 때문입니다.
이번 예시 뿐만 아니라 의존성은 전체 개체 대신 특정 속성을 의존성을 사용해야 합니다.
import React, { useEffect, useState, useReducer } from "react";
import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";
// reducer 함수로 state 업데이트
const emailReducer = (state, action) => {
if (action.type === "USER_INPUT") {
return { value: action.payload, isValid: action.payload.includes("@") };
}
if (action.type === "INPUT_BLUR") {
return { value: state.value, isValid: action.payload.includes("@") };
}
return { value: "", isValid: false };
};
const passwordReducer = (state, action) => {
if (action.type === "USER_INPUT") {
return { value: action.payload, isValid: action.payload.trim().length > 6 };
}
if (action.type === "INPUT_BLUR") {
return { value: state.value, isValid: action.payload.trim().length > 6 };
}
return { value: "", isValid: false };
};
// Login 컴포넌트
const Login = (props) => {
const [formIsValid, setFormIsValid] = useState(false);
// useReducer로 처음 state 초기화
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: "",
isValid: null,
});
const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
value: "",
isValid: null,
});
// 별칭 할당을 사용한 객체 Destructuring
const { isValid: emailIsValid } = emailState;
const { isValid: passwordIsValid } = passwordState;
// state에서 isValid 값만 사용해 버튼 활성 및 비활성
useEffect(() => {
const identifier = setTimeout(() => {
setFormIsValid(emailState.isValid && passwordState.isValid);
}, 300);
return () => {
clearTimeout(identifier);
};
// 별칭 할당을 사용한 객체 Destructuring을 통해 state의 isValid만 의존성으로 사용해서 의미없는 useEffect 실행을 차단
}, [emailIsValid, passwordIsValid]);
// handler에서 dispatch를 통해 reducer로 action을 전달
const emailChangeHandler = (event) => {
dispatchEmail({ type: "USER_INPUT", payload: event.target.value });
};
const passwordChangeHandler = (event) => {
dispatchPassword({ type: "USER_INPUT", payload: event.target.value });
};
const validateEmailHandler = () => {
dispatchEmail({ type: "INPUT_BLUR" });
};
const validatePasswordHandler = () => {
dispatchPassword({ type: "INPUT_BLUR" });
};
const submitHandler = (event) => {
event.preventDefault();
props.onLogin(emailState.value, passwordState.value);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailState.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div
className={`${classes.control} ${
passwordState.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={passwordState.value}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
</div>
<div className={classes.actions}>
<Button type="submit" className={classes.btn} disabled={!formIsValid}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;