combineReducers()
함수를 이용해 여러 리듀서를 하나로 합쳐주어야한다.modules/index.js
import { combineReducers } from "redux"; // import combineReducers
import counter from "./Counter"; // import reducers
import todos from "./Todos";
// combineReducers 함수의 매개변수에 import한 reducer를 넣어준다.
const rootReducer = combineReducers({
counter,
todos
})
export default rootReducer;
// 파일 이름을 index.js로 설정하면,
// import 시 디렉터리 이름까지만 입력하여 import 할 수 있다.
// 예) import rootReducer from './modules';
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'; // import createStore function
import './index.css';
import App from './App';
import rootReducer from './modules'; // import toorReducer
// createStore 함수에 매개변수로 rootReducer를 전달하여 store 생성
const store = createStore(rootReducer);
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
react-redux
에서 제공하는 Provider
컴포넌트로 감싸주어야한다. Provider
컴포넌트를 사용할 때는 store
를 props
로 전달해 주어야한다.import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux'; // import Provider component
import './index.css';
import App from './App';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
npm i redux-devtools-extension
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension'; // import
import './index.css';
import App from './App';
import rootReducer from './modules';
const store = createStore(rootReducer, composeWithDevTools()); // 적용
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
connect
함수를 사용해야한다.connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
mapStateToProps
: 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수
mapDispatchToProps
: 액션 생성함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수
connect 함수는 또 다른 함수를 반환하는데, 이 반환된 함수에 컴포넌트를 파라미터로 넣어주면 리덕스와 연동된 컴포넌트가 만들어진다. 위코드를 쉽게풀면 아래와 같다.
const makeContainer = connect(mapStateToProps, mapDispatchToProps);
makeContainer(연동할 컴포넌트);
mapStateToProps
와 mapDispatchToProps
에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달된다.mapStateToProps
는 state를 파라미터로 받아오며, 이 값은 현재 스토어가 갖고 있는 상태값을 가리킨다.mapDispatchToProps
의 경우 store의 내장함수 dispatch를 파리미터로 받아온다.import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/Counter';
// mapStateToProps, mapDispatchToProps가 반환하는 값이
// CounterContainer의 props 자동 전달된다.(in connect 함수)
const CounterContainer = ({number, increase, decrease}) => {
return (
<Counter
number={number}
onIncrease={increase}
onDecrease={decrease}
/>
);
};
// 스토어의 state를 파라미터로 받아온다.
const mapStateToProps = state => ({
number: state.counter.number
})
// 스토어의 dispatch 함수를 파라미터로 받아온다.
const mapDispatchToProps = dispatch => ({
increase: () => {
// 액션 생성함수를 import하여 사용
dispatch(increase());
},
decrease: () => {
dispatch(decrease());
}
})
// connect 함수로 컴포넌트와 리덕스 연결 => 컨테이너 컴포넌트 생성
export default connect(
mapStateToProps,
mapDispatchToProps
)(CounterContainer);
mapStateToProps
와 mapDispatchToProps
를 미리 선언하고 사용한다. 하지만 connect 내부에 익명 함수 형태로 선언하면 코드가 조금 더 깔끔해진다....
export default connect(
state => ({
number: state.counter.number
}),
dispatch => ({
increase: () => dispatch(increase()),
decrease: () => dispatch(decrease())
})
)(CounterContainer);
bindActionCreators
함수를 사용하면 간편하게 액션을 디스패치 할 수 있다....
import { bindActionCreators } from 'redux';
export default connect(
state => ({
number: state.counter.number
}),
dispatch => bindActionCreators(
{increase, decrease},
dispatch
)
)(CounterContainer);
...
export default connect(
state => ({
number: state.counter.number
}),
{
increase,
decrease
}
)(CounterContainer);
npm i redux-actions
로 설치createAction(actionType[, payloadFunction])
액션타입과 payload(액션에 필요한 추가 데이터)를 반환하는 함수를 전달받아 액션객체를 반환하는 함수
// import createAction
import { createAction } from 'redux-actions'
// 액션 타입 정의
const MY_ACTION = 'sample/MY_ACTION';
// createAction함수로 액션 생성 함수 만들기
const myAction = createAction(MY_ACTION);
// 결과 : { type: MY_ACTION }
// payload 추가하기
const action = myAction('hello');
// 결과 : { type: MY_ACTION, payload: 'hello' }
-----------------------------------------------------
// payload 정의 함수 사용 => payload 변형 가능
const myAction = createAction(MY_ACTION, text => `${text}!`);
const action = myAction('hello');
// 결과 : { type: MY_ACTION, payload: 'hello!' }
handleActions(updateObject, initialState)
리듀서 함수를 더 간단하고 가독성 높게 작성할 수 있도록 도와주는 함수
updateObject
: 액션에 따라 실행 할 함수들을 가지고있는 객체
initialState
: 기본상태값
예
const counter = handleActions(
{
[INCREASE]: (state, action) => ({number: state.number + 1}),
[DECREASE]: (state, action) => ({number: state.number - 1})
},
initialState
)
// modules/Todos.js 리듀서 예
// 액션 생성 함수는 액션에 필요한 추가 데이터를 payload라는 이름으로 사용하기 때문에
// 모두 공통적으로 action.payload로 값을 조회하도록 구현해야한다.
const todos = handleActions(
{
[CHANGE_INPUT]: (state, action) => ({...state, input: action.payload}),
[INSERT]: (state, action) => ({
...state,
todos: state.todos.concat(action.payload)
}),
[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
)
// 비구조화 할당 문법으로 action의 payload 이름을 새로 설정하면
// action.payload가 정확히 어떤 값을 의미하는지 더 쉽게 파악할 수 있다.
const todos = handleActions(
{
[CHANGE_INPUT]: (state, {payload: input}) => ({...state, input}),
[INSERT]: (state, {payload: todo}) => ({
...state,
todos: state.todos.concat(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
)
import produce from 'immer';
const nextState = produce(currentState, draft => {
draft.somwwhere.deep.inside = 5;
})
produce(currentState, recipe)
: 상태 불변성을 유지하여 새로운 상태를 생성하는 함수currentState
: 수정하고 싶은 상태recipe
: 첫 번째 파라미터인 currentState
를 어떻게 업데이트할지 정의하는 함수. recipe
함수는 원본 데이터(draft)를 매개변수로 전달받는다. 이 함수 내부에서는 불변성에 신경 쓰지 않는 것처럼 코드를 작성하여 상태를 변경한다.produce
함수는 recipe
함수 내부에서 변경한 상태를 바탕으로 새로운 상태를 반환한다.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
)
const 결과 = useSelector(상태 선택 함수);
import React from 'react';
import { useSelector } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/Counter';
const CounterContainer = () => {
const number = useSelector(state => state.counter.number);
return (
<Counter number={number} />
);
};
export default CounterContainer;
const dispatch = useDispatch();
dispatch({type: 'SAMPLE_ACTION'});
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Counter from '../components/Counter';
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;
// 최적화를 위해 useCallback 사용
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}
/>
);
};
import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/Todos';
import Todos from '../components/Todos'
const TodosContainers = () => {
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}
/>
);
};
const store = useStore();
store.dispatch({type: 'SAMPLE_ACTION'});
store.getState();
useACtions
Hook은 원래 react-redux에 내장된 상태로 릴리즈 될 계획이었으나 리덕스 개발 팀에서 꼭 필요하지 않다고 판단하여 제외되었다. 그 대신 공식 문서에서 그대로 복사하여 사용할 수 있도록 제공하고 있다.useActions
코드 (src/lib/useActions.js로 저장)import { bindActionCreators } from 'redux'
import { useDispatch } from 'react-redux'
import { useMemo } from 'react'
export function useActions(actions, deps) {
const dispatch = useDispatch()
return useMemo(
() => {
if (Array.isArray(actions)) {
return actions.map(a => bindActionCreators(a, dispatch))
}
return bindActionCreators(actions, dispatch)
},
deps ? [dispatch, ...deps] : [dispatch]
)
}
useAcrions
사용 예제import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/Todos';
import Todos from '../components/Todos';
import { useActions } from '../lib/useActions';
const TodosContainers = () => {
const { input, todos } = useSelector(({todos}) => ({
input: todos.input,
todos: todos.todos
}));
// useActions : 액션 생성 함수로 이루어진 배열과 dependency 배열을 전달받아
// 액션을 디스 패치하는 함수 배열을 반환하는 함수
// dependency 배열 원소가 바뀌면 액션을 디스패치하는 함수를 새로 만든다.
const [ onChangeInput, onInsert, onToggle, onRemove ] = useActions(
[changeInput, insert, toggle, remove],
[]
)
return (
<Todos
input={input}
todos={todos}
onChangeInput={onChangeInput}
onInsert={onInsert}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
connect
함수를 사용하여 컨테이너 컴포너트를 만들 경우, 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리랜더링 될 때 해당 컴포넌트의 props가 바뀌지 않는다면 리랜더링이 자동으로 방지 되어 성능이 최적화 된다.useSelector
를 사용하여 리덕스 상태를 조회했을 때는 이 최적화 작업이 이루어지지 않으므로, 성능 최적화를 위해 React.memo
를 사용해야 한다.