리덕스 더 편하게 사용하기
< 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 |