const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
2) todo
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋 값을 변경함
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제함
const REMOVE = 'todos/REMOVE'; // todo를 제거함
export const increase = ()=>({ type: INCREASE});
export const decrease = ()=>({ type: DECREASE});
2) todo: 파라미터가 존재하는 경우( 전달해줄 게 있는 경우) : 값을 변형시킨다.
export const changeInput = input => ({
type: CHANGE_INPUT,
input
});
let id = 3; // insert가 호출될 때마다 1씩 더해집니다.
export const insert = text => ({
type: INSERT,
todo: {
id: id++,
text,
done: false
}
});
export const toggle = id => ({
type: TOGGLE,
id
});
export const remove = id => ({
type: REMOVE,
id
});
3) createAction으로 만들기
(모듈은 미리 추가해두기)
import { createAction } from 'redux-actions';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
3-2) 무언가 추가할 값이 있을 경우.
export const insert = createAction(INSERT, text => ({
id: id++,
text,
done: false,
}));
const initialState = {
number: 0
};
2) 깊은 객체 (추후 리듀서 함수에서 변경시 spead 연산자를 사용하거나, immer 라이브러리를 사용한다)
const initialState = {
input: '',
todos: [
{
id: 1,
text: '리덕스 기초 배우기',
done: true
},
{
id: 2,
text: '리액트와 리덕스 사용하기',
done: false
}
]
};
function counter(state = initialState, action) {
switch (action.type) {
case INCREASE:
return {
number: state.number + 1
};
case DECREASE:
return {
number: state.number - 1
};
default:
return state;
}
}
export default counter;
방법1-2. 깊은 객체 바꾸기
function todos(state = initialState, action) {
switch (action.type) {
case CHANGE_INPUT:
return {
...state,
input: action.input
};
case INSERT:
return {
...state,
todos: state.todos.concat(action.todo)
};
case TOGGLE:
return {
...state,
todos: state.todos.map(todo =>
todo.id = = = action.id ? { ...todo, done: !todo.done } : todo
)
};
case REMOVE:
return {
...state,
todos: state.todos.filter(todo => todo.id != = action.id)
};
default:
return state;
}
}
export default todos;
방법3: handleAction 모듈 사용하기
(모듈은 미리 import 해두기)
import { createAction, handleActions } from 'redux-actions';
...
const counter = handleActions(
{
[INCREASE]: (state, action) => ({ number: state.number + 1 }),
[DECREASE]: (state, action) => ({ number: state.number - 1 }),
},
initialState,
);
export default counter;
import {combineReducers} from 'redux'
2) 다른 리듀서 모듈 가져오기
import counter from './counter'
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
import {createStore} from 'redux';
import rootReducer from './modules';
import {Provider} from 'react-redux';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
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'),
);
yarn add redux-devtools-extension
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
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'),
);
import {connect} from 'react-redux';
(ContainerCounter.js)
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
const mapStateToProps = state => ({
number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
increase: () => {
dispatch(increase());
},
decrease: () => {
dispatch(decrease());
},
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CounterContainer);
2) 익명함수로 한 번에 정의하기
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
export default connect(
state => ({
number: state.counter.number,
}),
dispatch => ({
increase: () => dispatch(increase()),
decrease: () => dispatch(decrease()),
}),
)(CounterContainer);
3) 2를 bindActionCreator를 사용하여 더욱 견고하게 만들기
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
export default connect(
state => ({
number: state.counter.number,
}),
dispatch =>
bindActionCreators(
{
increase,
decrease,
},
dispatch,
),
)(CounterContainer);
4) 가장 간단한 방법: mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션생성함수로 이루어진 객체 형태로 넣어주기
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
export default connect(
state => ({
number: state.counter.number,
}),
{
increase,
decrease,
},
)(CounterContainer);
(state을 props로 바로 받아오는 경우 )
export default connect(
state => ({
number: state.counter
}),
{
increase,
decrease
}
)(CounterContainer);
iimport CounterContainer from './containers/CounterContainer';
import TodosContainer from './containers/TodosContainer';
const App = () => {
return (
<div>
<CounterContainer />
<hr />
<TodosContainer />
</div>
);
};
export default App;
const TodoItem = ({ todo, onToggle, onRemove }) => {
return (
<div>
<input
type="checkbox"
onClick={() => onToggle(todo.id)}
checked={todo.done}
readOnly={true}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => onRemove(todo.id)}>삭제</button>
</div>
);
};
const Todos = ({
input, // 인풋에 입력되는 텍스트
todos, // 할 일 목록이 들어 있는 객체
onChangeInput,
onInsert,
onToggle,
onRemove,
}) => {
const onSubmit = e => {
e.preventDefault();
onInsert(input);
onChangeInput(''); // 등록 후 인풋 초기화
};
const onChange = e => onChangeInput(e.target.value);
return (
<div>
<form onSubmit={onSubmit}>
<input value={input} onChange={onChange} />
<button type="submit">등록</button>
</form>
<div>
{todos.map(todo => (
<TodoItem
todo={todo}
key={todo.id}
onToggle={onToggle}
onRemove={onRemove}
/>
))}
</div>
</div>
);
};
export default Todos;
yarn add immer
import produce from 'immer'
const nextState = produce(originalState, draft => {
// 바꾸고 싶은 값 바꾸기
draft.somewhere.deep.inside = 5;
})
(리듀서와 함께)
import { createAction, handleActions } from 'redux-actions';
import produce from 'immer';
(...)
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;
(react-redux 만 설치하면 됨)
import {useSelector} from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
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);
const dispatch = useDispatch();
return (
<Counter
number={number}
onIncrease={() => dispatch(increase())}
onDecrease={() => dispatch(decrease())}
/>
);
};
export default CounterContainer;
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 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;
import React, { useCallback } 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();
const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
return (
<Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
);
};
export default CounterContainer;
connect
컨테이너 컴포넌트의 props가 바뀌지 않는다면 리렌더링이 자동으로 방지됨
useSelector
자동으로 이뤄지지 않으므로 React.memo 필수
(아래 예시는 app 컴포넌트가 리렌더링되는 일이 없을 테므로 불필요 하지만, 하여튼 말이 그렇다는 거다.)
import React 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 TodosContainer = () => {
(...)
};
export default React.memo(TodosContainer);
yarn add redux-actions
import { createStore, applyMiddleware } from 'redux';
const loggerMiddleware = store => next => action => {
console.group(action && action.type); // 액션 타입으로 log를 그룹화함
console.log('이전 상태', store.getState());
console.log('액션', action);
next(action); // 다음 미들웨어 혹은 리듀서에게 전달
console.log('다음 상태', store.getState()); // 업데이트된 상태
console.groupEnd(); // 그룹 끝
};
export default loggerMiddleware;
import loggerMiddleware from './lib/loggerMiddleware';
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
import loggerMiddleware from './lib/loggerMiddleware';
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
yarn add redux-logger
import { createLogger } from 'redux-logger';
const logger = createLogger();
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
// import loggerMiddleware from './lib/loggerMiddleware';
import { createLogger } from 'redux-logger';
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
export const increase = createAction(INCREASE);
(무언가 넣을 게 있다면)
export const insert = createAction(INSERT, text => ({
id: id++,
text,
done: false,
}));
(redux-thunk 모듈을 미들웨어에 넣은 리덕스모듈의 작성법)
export const increaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(increase());
}, 1000);
};
yarn add redux-thunk
import ReduxThunk from 'redux-thunk';
const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
// import loggerMiddleware from './lib/loggerMiddleware';
import { createLogger } from 'redux-logger';
import ReduxThunk from 'redux-thunk';
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
export const decreaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(decrease());
}, 1000);
};
(modules/counter.js 전체파일)
import { createAction, handleActions } from 'redux-actions';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
// 1초 뒤에 increase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(increase());
}, 1000);
};
export const decreaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(decrease());
}, 1000);
};
const initialState = 0; // 상태는 꼭 객체일 필요가 없습니다. 숫자도 작동해요.
const counter = handleActions(
{
[INCREASE]: state => state + 1,
[DECREASE]: state => state - 1
},
initialState
);
export default counter;
import { connect } from 'react-redux';
import { increaseAsync, decreaseAsync } from '../modules/counter';
import Counter from '../components/Counter';
const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
return (
<Counter
number={number}
onIncrease={increaseAsync}
onDecrease={decreaseAsync}
/>
);
};
export default connect(
state => ({
number: state.counter
}),
{
increaseAsync,
decreaseAsync
}
)(CounterContainer);
yarn add axios
import axios from 'axios';
export const getPost = id =>
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
export const getUsers = id =>
axios.get(`https://jsonplaceholder.typicode.com/users`);
import { handleActions } from 'redux-actions';
import * as api from '../lib/api';
// 액션 타입을 선언합니다.
// 한 요청당 세 개를 만들어야 합니다.
const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';
const GET_POST_FAILURE = 'sample/GET_POST_FAILURE';
const GET_USERS = 'sample/GET_USERS';
const GET_USERS_SUCCESS = 'sample/GET_USERS_SUCCESS';
const GET_USERS_FAILURE = 'sample/GET_USERS_FAILURE';
export const getPost = id => async dispatch => {
dispatch({ type: GET_POST }); // 요청을 시작한 것을 알림
try {
const response = await api.getPost(id);
dispatch({
type: GET_POST_SUCCESS,
payload: response.data
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_POST_FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
}
};
export const getUsers = () => async dispatch => {
dispatch({ type: GET_USERS }); // 요청을 시작한 것을 알림
try {
const response = await api.getUsers();
dispatch({
type: GET_USERS_SUCCESS,
payload: response.data
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_USERS_FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
}
};
// 초기 상태를 선언합니다.
// 요청의 로딩 중 상태는 loading이라는 객체에서 관리합니다.
const initialState = {
loading: {
GET_POST: false,
GET_USERS: false
},
post: null,
users: null
};
const sample = handleActions(
{
[GET_POST]: state => ({
...state,
loading: {
...state.loading,
GET_POST: true // 요청 시작
}
}),
[GET_POST_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false // 요청 완료
},
post: action.payload
}),
[GET_POST_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false // 요청 완료
}
}),
[GET_USERS]: state => ({
...state,
loading: {
...state.loading,
GET_USERS: true // 요청 시작
}
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false // 요청 완료
},
users: action.payload
}),
[GET_USERS_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false // 요청 완료
}
})
},
initialState
);
export default sample;
import { handleActions } from 'redux-actions';
import * as api from '../lib/api';
// 액션 타입을 선언합니다.
// 한 요청당 세 개를 만들어야 합니다.
const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';
const GET_POST_FAILURE = 'sample/GET_POST_FAILURE';
const GET_USERS = 'sample/GET_USERS';
const GET_USERS_SUCCESS = 'sample/GET_USERS_SUCCESS';
const GET_USERS_FAILURE = 'sample/GET_USERS_FAILURE';
// thunk 함수를 생성합니다.
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치합니다.
export const getPost = id => async dispatch => {
dispatch({ type: GET_POST }); // 요청을 시작한 것을 알림
try {
const response = await api.getPost(id);
dispatch({
type: GET_POST_SUCCESS,
payload: response.data
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_POST_FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
}
};
export const getUsers = () => async dispatch => {
dispatch({ type: GET_USERS }); // 요청을 시작한 것을 알림
try {
const response = await api.getUsers();
dispatch({
type: GET_USERS_SUCCESS,
payload: response.data
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_USERS_FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
}
};
// 초기 상태를 선언합니다.
// 요청의 로딩 중 상태는 loading이라는 객체에서 관리합니다.
const initialState = {
loading: {
GET_POST: false,
GET_USERS: false
},
post: null,
users: null
};
const sample = handleActions(
{
[GET_POST]: state => ({
...state,
loading: {
...state.loading,
GET_POST: true // 요청 시작
}
}),
[GET_POST_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false // 요청 완료
},
post: action.payload
}),
[GET_POST_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false // 요청 완료
}
}),
[GET_USERS]: state => ({
...state,
loading: {
...state.loading,
GET_USERS: true // 요청 시작
}
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false // 요청 완료
},
users: action.payload
}),
[GET_USERS_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false // 요청 완료
}
})
},
initialState
);
export default sample;
import { combineReducers } from 'redux';
import counter from './counter';
import sample from './sample';
const rootReducer = combineReducers({
counter,
sample
});
export default rootReducer;
// post
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
// users
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
(...)
]
const Sample = ({ loadingPost, loadingUsers, post, users }) => {
return (
<div>
<section>
<h1>포스트</h1>
{loadingPost && '로딩 중...'}
{!loadingPost && post && (
<div>
<h3>{post.title}</h3>
<h3>{post.body}</h3>
</div>
)}
</section>
<hr />
<section>
<h1>사용자 목록</h1>
{loadingUsers && '로딩 중...'}
{!loadingUsers && users && (
<ul>
{users.map(user => (
<li key={user.id}>
{user.username} ({user.email})
</li>
))}
</ul>
)}
</section>
</div>
);
};
export default Sample;
import { connect } from 'react-redux';
import Sample from '../components/Sample';
import { getPost, getUsers } from '../modules/sample';
const { useEffect } = React;
2) 컨테이너에서 가져올 props 정의하기
const SampleContainer = ({
getPost,
getUsers,
post,
users,
loadingPost,
loadingUsers
}) => {
// 클래스 형태 컴포넌트였다면 componentDidMount
useEffect(() => {
getPost(1);
getUsers(1);
}, [getPost, getUsers]);
return (
<Sample
post={post}
users={users}
loadingPost={loadingPost}
loadingUsers={loadingUsers}
/>
);
};
4) connect 함수 사용하여 적용하기
export default connect(
({ sample }) => ({
post: sample.post,
users: sample.users,
loadingPost: sample.loading.GET_POST,
loadingUsers: sample.loading.GET_USERS
}),
{
getPost,
getUsers
}
)(SampleContainer);
(전체 파일)
import { connect } from 'react-redux';
import Sample from '../components/Sample';
import { getPost, getUsers } from '../modules/sample';
const { useEffect } = React;
const SampleContainer = ({
getPost,
getUsers,
post,
users,
loadingPost,
loadingUsers
}) => {
// 클래스 형태 컴포넌트였다면 componentDidMount
useEffect(() => {
getPost(1);
getUsers(1);
}, [getPost, getUsers]);
return (
<Sample
post={post}
users={users}
loadingPost={loadingPost}
loadingUsers={loadingUsers}
/>
);
};
export default connect(
({ sample }) => ({
post: sample.post,
users: sample.users,
loadingPost: sample.loading.GET_POST,
loadingUsers: sample.loading.GET_USERS
}),
{
getPost,
getUsers
}
)(SampleContainer);
import SampleContainer from './containers/SampleContainer';
const App = () => {
return (
<div>
<SampleContainer />
</div>
);
};
export default App;
export const getUsers = () => async dispatch => {
dispatch({ type: GET_USERS }); // 요청을 시작한 것을 알림
try {
const response = await api.getUsers();
dispatch({
type: GET_USERS_SUCCESS,
payload: response.data
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_USERS_FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
}
};
export const getPost = id => async dispatch => {
dispatch({ type: GET_POST }); // 요청을 시작한 것을 알림
try {
const response = await api.getPost(id);
dispatch({
type: GET_POST_SUCCESS,
payload: response.data
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_POST_FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
}
};
(리팩토링)
export default function createRequestThunk(type, request) {
// 성공 및 실패 액션 타입을 정의합니다.
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return params => async dispatch => {
dispatch({ type }); // 시작됨
try {
const response = await request(params);
dispatch({
type: SUCCESS,
payload: response.data
}); // 성공
} catch (e) {
dispatch({
type: FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e;
}
};
}
// 사용법: createRequestThunk('GET_USERS',api.getUsers);
(리듀서 모듈에 적용하기 )
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);
(로딩 리듀서 추가하기)
import { createAction, handleActions } from 'redux-actions';
const START_LOADING = 'loading/START_LOADING';
const FINISH_LOADING = 'loading/FINISH_LOADING';
/*
요청을 위한 액션 타입을 payload로 설정합니다(예: "sample/GET_POST").
*/
export const startLoading = createAction(
START_LOADING,
requestType => requestType
);
export const finishLoading = createAction(
FINISH_LOADING,
requestType => requestType
);
const initialState = {};
const loading = handleActions(
{
[START_LOADING]: (state, action) => ({
...state,
[action.payload]: true
}),
[FINISH_LOADING]: (state, action) => ({
...state,
[action.payload]: false
})
},
initialState
);
export default loading;
{
type: 'loading/START_LOADING',
payload: 'sample/GET_POST'
}
{'sample/GET_POST': true}
{
type: 'loading/FINISH_LOADING',
payload: 'sample/GET_POST'
}
{'sample/GET_POST': false}
import { combineReducers } from 'redux';
import counter from './counter';
import sample from './sample';
import loading from './loading';
const rootReducer = combineReducers({
counter,
sample,
loading
});
export default rootReducer;
(이하 전부 리팩토링)
3. lib/createRequestThunk.js 에서 loading 함수 사용하기
import { startLoading, finishLoading } from '../modules/loading';
export default function createRequestThunk(type, request) {
// 성공 및 실패 액션 타입을 정의합니다.
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return params => async dispatch => {
dispatch({ type }); // 시작됨
dispatch(startLoading(type));
try {
const response = await request(params);
dispatch({
type: SUCCESS,
payload: response.data
}); // 성공
dispatch(finishLoading(type));
} catch (e) {
dispatch({
type: FAILURE,
payload: e,
error: true
}); // 에러 발생
dispatch(startLoading(type));
throw e;
}
};
}
// 사용법: createRequestThunk('GET_USERS',api.getUsers);
loading['sample/GET_POST'] //true 혹은 false를 반환한다.
(containers/sampleContainer.js)
import { connect } from 'react-redux';
import Sample from '../components/Sample';
import { getPost, getUsers } from '../modules/sample';
const { useEffect } = React;
const SampleContainer = ({
getPost,
getUsers,
post,
users,
loadingPost,
loadingUsers
}) => {
// 클래스 형태 컴포넌트였다면 componentDidMount
useEffect(() => {
getPost(1);
getUsers(1);
}, [getPost, getUsers]);
return (
<Sample
post={post}
users={users}
loadingPost={loadingPost}
loadingUsers={loadingUsers}
/>
);
};
export default connect(
({ sample, loading }) => ({
post: sample.post,
users: sample.users,
loadingPost: loading['sample/GET_POST'],
loadingUsers: loading['sample/GET_USER']
}),
{
getPost,
getUsers
}
)(SampleContainer);
(성공시에만 success 액션으로 action.payload 값을 가져온다. )
const GET_POST_FAILURE ='sample/GET_POST_FAILURE'
//loading 은 loading 에서 관리하기 때문이다.
const initialState = {
loading: {
GET_POST: false,
GET_USERS: false
},
post: null,
users: null
};
import { handleActions } from 'redux-actions';
import * as api from '../lib/api';
import createRequestThunk from '../lib/createRequestThunk';
// 액션 타입을 선언합니다.
const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';
const GET_USERS = 'sample/GET_USERS';
const GET_USERS_SUCCESS = 'sample/GET_USERS_SUCCESS';
// thunk 함수를 생성합니다.
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치합니다.
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);
// 초기 상태를 선언합니다.
// 요청의 로딩 중 상태는 loading이라는 객체에서 관리합니다.
const initialState = {
post: null,
users: null
};
const sample = handleActions(
{
[GET_POST_SUCCESS]: (state, action) => ({
...state,
post: action.payload
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
users: action.payload
})
},
initialState
);
export default sample;
(sampleContainer.js)
useEffect(() => {
// useEffect에 파라미터로 넣는 함수는 async로 할 수 없기 때문에
// 그 내부에서 async 함수를 선언하고 호출해 줍니다.
const fn = async () => {
try {
await getPost(1);
await getUsers(1);
} catch (e) {
console.log(e); // 에러 조회
}
};
fn();
}, [getPost, getUsers]);