리덕스 더 편하게 사용하기
< redux-actions >
redux-action을 사용하면 액션 생성 함수를 더 짧은 코드를 생성할 수 있습니다. 그리고 리듀서를 작성할 때도 switch/case문이 아닌 handleActions라는 함수를 사용하여 각 액션마다 업데이트 함수를 설정하는 형식으로 작성해 줄 수 있습니다.
< counter module >
// src/modules/counter.js
// 앞에 모듈 이름을 붙여 주는 것은 관례에고 익션 이름을 대문자로 하는 것 또한 관례입니다.
import { createAction, handleActions } from "redux-actions";
// 액션 타입 정의
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
// 액션 생성 함수 만들기
// 추후 다른 파일에서도 사용하기 위해 export해준다.
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
// 초기 상태 => number = 0
const initialState = {
number: 0,
};
const counter = handleActions(
{
[INCREASE]: (state, action) => ({ number: state.number + 1 }),
[DECREASE]: (state, action) => ({ number: state.number - 1 }),
},
initialState
);
export default counter;
< todo module >
// src/modules/todos.js
import { createAction, handleActions } from "redux-actions";
const CHANGE_INPUT = "todos/CHANGE_INPUT";
const INSERT = "todos/INSERT";
const TOGGLE = "todos/TOGGLE";
const REMOVE = "todos/REMOVE";
export const changeInput = createAction(CHANGE_INPUT, (input) => input);
let id = 3;
export const insert = createAction(INSERT, (text) => ({
id: id++,
text,
done: false,
}));
export const toggle = createAction(TOGGLE, (id) => id);
export const remove = createAction(REMOVE, (id) => id);
const initialState = {
input: "",
todos: [
{
id: 1,
text: "리덕스 기초 배우기",
done: true,
},
{
id: 2,
text: "리액트와 리덕스 사용하기",
done: false,
},
],
};
const todos = handleActions(
{
[CHANGE_INPUT]: (state, action) => ({ ...state, input: action.input }),
[INSERT]: (state, action) => ({
...state,
todos: [...state.todos, action.todo],
}),
[TOGGLE]: (state, action) => ({
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload ? { ...todo, done: !todo.done } : todo
),
}),
[REMOVE]: (state, action) => ({
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
}),
},
initialState
);
export default todos;
와 같이 작성할 수 있습니다.
todos module에는 액션에 필요한 추가 데이터가 counter module과는 다르게 존재합니다.
createAction으로 액션을 만들면 액션에 추가 데이터는 payload라는 이름을 사용합니다.
const MY_ACTION = 'sample/MY_ACTION'
const myAction = createAction(MY_ACTION, text => `${text}!`);
const action = myAction('hello, world!')
/*
결과:
{ type: MY_ACTION, payload: 'hello world!' }
*/
또한 todo module은 아래와 같이 보기좋게 수정할 수도 있습니다.
// src/modules/todos.js
import { createAction, handleActions } from "redux-actions";
const CHANGE_INPUT = "todos/CHANGE_INPUT";
const INSERT = "todos/INSERT";
const TOGGLE = "todos/TOGGLE";
const REMOVE = "todos/REMOVE";
export const changeInput = createAction(CHANGE_INPUT, (input) => input);
let id = 3;
export const insert = createAction(INSERT, (text) => ({
id: id++,
text,
done: false,
}));
export const toggle = createAction(TOGGLE, (id) => id);
export const remove = createAction(REMOVE, (id) => id);
const initialState = {
input: "",
todos: [
{
id: 1,
text: "리덕스 기초 배우기",
done: true,
},
{
id: 2,
text: "리액트와 리덕스 사용하기",
done: false,
},
],
};
const todos = handleActions(
{
[CHANGE_INPUT]: (state, { payload: input }) => ({
...state,
input: input,
}),
[INSERT]: (state, { payload: todo }) => ({
...state,
todos: [...state.todos, todo],
}),
[TOGGLE]: (state, { payload: id }) => ({
...state,
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
),
}),
[REMOVE]: (state, { payload: id }) => ({
...state,
todos: state.todos.filter((todo) => todo.id !== id),
}),
},
initialState
);
export default todos;
객체 비구조화 할당 문법으로 action 값의 payload이름을 새로 설정해 주면 action.payload가 정확히 어떤 값을 의미하는지 더 쉽게 파악할 수 있습니다.
< immer >
redux의 state를 바꿀 떄 state의 불변성을 지켜주어야 합니다. 따라서 spread 연산자(...)와 배열의 내장 함수를 활용했었습니다. 그러나 모듈의 상태가 복잡해질수록 불변성을 지키기가 까다로워집니다.
따라서 간단한 counter module말고 todos module을 immer를 통해 바꾸어 봅니다.
// src/modules/todos.js
import { createAction, handleActions } from "redux-actions";
import produce from "immer";
const CHANGE_INPUT = "todos/CHANGE_INPUT";
const INSERT = "todos/INSERT";
const TOGGLE = "todos/TOGGLE";
const REMOVE = "todos/REMOVE";
export const changeInput = createAction(CHANGE_INPUT, (input) => input);
let id = 3;
export const insert = createAction(INSERT, (text) => ({
id: id++,
text,
done: false,
}));
export const toggle = createAction(TOGGLE, (id) => id);
export const remove = createAction(REMOVE, (id) => id);
const initialState = {
input: "",
todos: [
{
id: 1,
text: "리덕스 기초 배우기",
done: true,
},
{
id: 2,
text: "리액트와 리덕스 사용하기",
done: false,
},
],
};
const todos = handleActions(
{
[CHANGE_INPUT]: (state, { payload: input }) =>
produce(state, (draft) => {
draft.input = input;
}),
[INSERT]: (state, { payload: todo }) =>
produce(state, (draft) => {
draft.todos.push(todo);
}),
[TOGGLE]: (state, { payload: id }) =>
produce(state, (draft) => {
const todo = draft.todos.find((todo) => todo.id === id);
todo.done = !todo.done;
}),
[REMOVE]: (state, { payload: id }) =>
produce(state, (draft) => {
const index = draft.todos.findIndex((todo) => todo.id === id);
draft.todos.splice(index, 1);
}),
},
initialState
);
export default todos;
Hooks를 사용하여 컨테이너 컴포넌트 만들기
< useSelector >
useSelector Hook을 사용하면 connect함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있습니다.
import React from "react";
import Counter from "../components/Counter";
import { connect, useSelector } from "react-redux";
import { increase, decrease } from "../modules/counter";
const CounterContainer = ({ number, increase, decrease }) => {
const number = useSelector((state) => state.counter.number);
return <Counter number={number} onIncrease={increase} onDecrease={decrease} />;
};
export default connect(({ counter }) => ({ number: counter.number }), {
increase,
decrease,
})(CounterContainer);
< useDispatch >
useDispatch는 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해줍니다.
import React from "react";
import Counter from "../components/Counter";
import { useSelector, useDispatch } from "react-redux";
import { increase, decrease } from "../modules/counter";
const CounterContainer = () => {
const number = useSelector((state) => state.counter.number);
const dispatch = useDispatch();
return (
<Counter
number={number}
onIncrease={() => dispatch(increase())}
onDecrease={() => dispatch(decrease())}
/>
);
};
export default CounterContainer;
이러한 상황에서 숫자가 바뀌어서 컴포넌트라 리렌더링될 때마다 onIncrease함수와 onDecrease함수가 세롭게 만들어지고 있습니다. 만약 컴포넌트의 성능을 최적화해야 하는 상황이 온다면 useCallback으로 액션을 디스패치하는 함수를 감싸 주는 것이 좋습니다.
import React, { useCallback } from "react";
import Counter from "../components/Counter";
import { useSelector, useDispatch } from "react-redux";
import { increase, decrease } from "../modules/counter";
const CounterContainer = () => {
const number = useSelector((state) => state.counter.number);
const dispatch = useDispatch();
const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
return <Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />;
};
export default CounterContainer;
useCallback을 사용화하는 습관을 들입시다!!
< useStore >
useStore Hooks는 컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있습니다. 정말 어쩌다가 스토어에 직접 접근해야 하는 상황에만 사용합시다. 사용 예제는 아래와 같습니다.
const store = useStore();
store.dispatch({ type: 'SAMPLE_ACTION' });
store.getState();
컨테이너들의 Hooks으로의 전환
< TodosContainer Using Hooks Version >
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import Todos from "../components/Todos";
import { changeInput, insert, toggle, remove } from "../modules/todos";
const TodosContainer = () => {
const { input, todos } = useSelector(({ todos }) => ({
input: todos.input,
todos: todos.todos,
}));
const dispatch = useDispatch();
const onChangeInput = useCallback(
(input) => dispatch(changeInput(input)),
[dispatch]
);
const onInsert = useCallback((text) => dispatch(insert(text)), [dispatch]);
const onToggle = useCallback((id) => dispatch(toggle(id)), [dispatch]);
const onRemove = useCallback((id) => dispatch(remove(id)), [dispatch]);
return (
<Todos
input={input}
todos={todos}
onChangeInput={onChangeInput}
onInsert={onInsert}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
export default TodosContainer;
< React.memo >
useSelector 컨테이너 컴포넌트를 만들었을 경우, 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링될 떄 해당 컴포넌트의 props가 바뀌지 않았더라면 리렌더링이 자동으로 방지 되게 해야 합니다. ( connect, useDispatch는 자동으로 방지 )
아래와 같이 HOC인 React.memo를 활용해야 합니다.
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import Todos from "../components/Todos";
import { changeInput, insert, toggle, remove } from "../modules/todos";
const TodosContainer = () => {
const { input, todos } = useSelector(({ todos }) => ({
input: todos.input,
todos: todos.todos,
}));
const dispatch = useDispatch();
const onChangeInput = useCallback(
(input) => dispatch(changeInput(input)),
[dispatch]
);
const onInsert = useCallback((text) => dispatch(insert(text)), [dispatch]);
const onToggle = useCallback((id) => dispatch(toggle(id)), [dispatch]);
const onRemove = useCallback((id) => dispatch(remove(id)), [dispatch]);
return (
<Todos
input={input}
todos={todos}
onChangeInput={onChangeInput}
onInsert={onInsert}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
export default React.memo(TodosContainer);
'React > ReactJs' 카테고리의 다른 글
React - Redux MiddleWare >> redux-saga (0) | 2022.02.22 |
---|---|
React - Redux MiddleWare >> default + redux-thunk (0) | 2022.02.22 |
React - redux (0) | 2022.02.21 |
React - Context API (0) | 2022.02.21 |
styled-component의 동작방식 (0) | 2022.02.21 |