useReducer는 무엇인가?
1. useReducer는 컴포넌트의 state를 관리할 수 있는 리액트 훅이다.
2. useReducer는 useState의 대안이기도 하다.
state값이 {
title: " ",
desc: " ",
price: 0,
category: " ",
tags: [],
quantity: 0,
}
이런식으로 되어있고 이를 일반적인 useState hook의 setState로 변경한다고 생각해보자.
현재의 컴포넌트에서 저렇게 많은 값을 관리하자니 코드가 굉장히 복잡해질 것이다.
거기다 non-primitive한 객체나 배열에 대해서는 {...state, ~}
이런 식으로 불변성 관리까지 더불어서 값을 수정해주어야한다.
즉 더욱 복잡해질 것 이다.
이를 useReducer로 해결 할 수 있다.
useReducer의 사용 예
data Fetch시에는 로딩, 에러, 성공일 때의 처리를 하고자 했을 때 아래와 같이 작성할 수 있다.
코드를 살펴보면 하나의 함수 내부에서 setter가 여러 번 발생된다.
useReducer 미사용시
function App() {
const [loading, setLoading] = useState(false);
const [post, setPost] = useState({
title: ''
});
const [error, setError] = useState(false);
const handleFetch = () => {
setLoading(true);
setError(false);
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((res) => {
return res.json();
})
.then((data) => {
setLoading(false);
setPost(data);
})
.catch((err) => {
setLoading(false);
if(err){
setError(true);
}
})
}
return (
<div className="App">
<button onClick={handleFetch}>Fetch the data</button>
{loading && <div>로딩중 ...</div>}
{post && <div>{post.title}</div>}
{error && <div>에러발생!</div>}
</div>
);
}
export default App;
이를 useReducer로 작성해보았다.
더 이상 setter가 반복되지 않는데 dispatch 를 통해서 reducer에게 액션 타입을 넘겨주어
state 관리를 외부에서 관리할 수 있게 만들었다.
useReducer 사용시 - 첫 번째 예시
App.tsx
function App() {
const [state, dispatch] = useReducer(postReducer, initialState);
const handleFetch = () => {
dispatch({type: POST.FETCH_LOADING});
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((res) => {
return res.json();
})
.then((data) => {
console.log('data',data);
dispatch({type: POST.FETCH_SUCCESS, payload: data});
})
.catch((err) => {
dispatch({type: POST.FETCH_FAILURE});
})
}
console.log('state',state);
return (
<div className="App">
<button onClick={handleFetch}>Fetch the data</button>
{state.loading && <div>로딩중 ...</div>}
{state && <div>{state.post.title}</div>}
{state.error && <div>에러발생!</div>}
</div>
);
}
export default App;
postReducer.ts
export const initialState = {
loading: false,
error: false,
post : {}
}
export enum POST {
FETCH_LOADING = 'FETCH_LOADING',
FETCH_SUCCESS = 'FETCH_SUCCESS',
FETCH_FAILURE = 'FETCH_FAILURE',
}
interface IActionType {
type: POST;
payload?: any;
}
export const postReducer = (state: typeof initialState, action: IActionType) => {
switch (action.type) {
case POST.FETCH_LOADING:
return {
...state,
error: false,
loading: true
};
case POST.FETCH_SUCCESS:
return {
...state,
post: action.payload,
loading: false
};
case POST.FETCH_FAILURE:
return{
...state,
loading: false,
error: true
};
default:
return state;
}
}
export default postReducer;
useReducer 사용시 - 두 번째 예시
Form.tsx
const Form = () => {
const [state, dispatch] = useReducer(formReducer, initialState);
const tagArea = useRef<HTMLTextAreaElement>(null)
const onHandleValue = (e:React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
dispatch({
type: FORM.CHANGE_INPUT,
payload: {name: e.target.name, value: e.target.value}
})
}
const onClickAddTags = () => {
const tagValue = tagArea.current?.value.split(',');
tagValue?.forEach(value => {
dispatch({
type: FORM.CHANGE_TAG,
payload: value
})
});
}
const onSubmitForm = (e:React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('state',state);
}
return (
<div>
<form onSubmit={onSubmitForm}>
<input onChange={(e) => onHandleValue(e)} name='title' type="text" placeholder='title'/>
<br/>
<input onChange={(e) => onHandleValue(e)} name='desc' type="text" placeholder='desc'/>
<br/>
<input onChange={(e) => onHandleValue(e)} name='price' type="number" placeholder='number'/>
<br/>
<select onChange={(e) => onHandleValue(e)} name='category'>
<option value="sneakers">sneakers</option>
<option value="tshirts">tshirts</option>
<option value="jeans">jeans</option>
</select>
<br/>
<textarea ref={tagArea} placeholder='tags with comma' name="tags"/>
<button onClick={onClickAddTags}>ADD TAGS</button>
<br/>
{state.tags.map((value: string) => (
<div key={value} onClick={() => dispatch({type: FORM.REMOVE_TAG, payload: value})}>{value}</div>
))}
<br/>
<div className="quantity">
<button onClick={() => dispatch({type: FORM.REMOVE_QUANTITY})}>-</button>
<span>Quantity {state.quantity}</span>
<button onClick={() => dispatch({type: FORM.ADD_QUANTITY})}>+</button>
</div>
<button type='submit'>Submit</button>
</form>
</div>
)
}
export default Form
formReducer.ts
export const initialState ={
title: "",
desc: "",
price: 0,
category: "",
tags: [],
quantity: 0,
}
export interface IinitialState {
title: string;
desc: string;
price: number;
category: string;
tags : string[];
quantity: number
}
export enum FORM{
CHANGE_INPUT = 'CHANGE_INPUT',
CHANGE_TAG = 'CHANGE_TAG',
REMOVE_TAG = 'REMOVE_TAG',
ADD_QUANTITY = 'ADD_QUANTITY',
REMOVE_QUANTITY = 'REMOVE_QUANTITY'
}
interface IActionType {
type: FORM,
payload?: any
}
const formReducer = (state: IinitialState, action:IActionType) => {
switch (action.type) {
case FORM.CHANGE_INPUT:
return{
...state,
[action.payload.name]: action.payload.value
}
case FORM.CHANGE_TAG:
return{
...state,
tags: [...state.tags, action.payload]
}
case FORM.REMOVE_TAG:
return {
...state,
tags: state.tags.filter(value => value !== action.payload)
}
case FORM.ADD_QUANTITY:
return{
...state,
quantity: state.quantity + 1
}
case FORM.REMOVE_QUANTITY:
return{
...state,
quantity: state.quantity - 1
}
default:
return state;
}
}
export default formReducer;
'React' 카테고리의 다른 글
React setState() 사용 시 알아둘 것 (0) | 2023.04.26 |
---|---|
useRef (0) | 2023.01.24 |
페이지 이동시 state를 동반하여 이동하기 (feat. useNavigate와 useLocation) (0) | 2022.09.12 |
페이지 이동시 화면 맨위로 스크롤하기(스크롤 애니메이션 없이) (0) | 2022.09.12 |
useRef를 이용하여 외부 클릭시 DropDown 메뉴 사라지게 하기 (0) | 2022.03.10 |