π₯Έ
42μΌμ°¨
Part 11. Redux λ‘ μνκ΄λ¦¬νκΈ°
Ch 2. Redux Advanced (1)
Ch 3. Redux Advanced (2)
Ch 2. Redux Advanced
π Async Action with Redux (not use λ―Έλ€μ¨μ΄)
API νΈμΆμ λ³΄ν΅ componentDidMount μμ μ ν΄μΌ νλ€.
$ npm i axios
// ./redux/action.js
...
// users
// κΉν API νΈμΆμ μμνλ κ²μ μλ―Έ
export const GET_USERS_START = "GET_USERS_START";
// κΉν API νΈμΆμ λν μλ΅μ΄ μ±κ³΅μ μΌλ‘ λμμ¨ κ²½μ°
export const GET_USERS_SUCCESS = "GET_USERS_SUCCESS";
// κΉν API νΈμΆμ λν μλ΅μ΄ μ€ν¨ν κ²½μ°
export const GET_USERS_FAIL = "GET_USERS_FAIL";
export function getUsersStart() {
return {
type: GET_USERS_START,
};
}
export function getUsersSuccess(data) {
return {
type: GET_USERS_SUCCESS,
data,
};
}
export function getUsersFali(error) {
return {
type: GET_USERS_FAIL,
error,
};
}
// ./redux/reducers/users.js
import { GET_USERS_FAIL, GET_USERS_START, GET_USERS_SUCCESS } from "../actions";
const initialState = { loading: false, data: [], error: null };
export default function users(state = initialState, action) {
if (action.type === GET_USERS_START) {
return {
...state,
loading: true,
error: null,
};
}
if (action.type === GET_USERS_SUCCESS) {
return {
...state,
loading: false,
data: action.data,
};
}
if (action.type === GET_USERS_FAIL) {
return {
...state,
loading: false,
error: action.error,
};
}
return state;
}
// ./redux/reducers/reducer.js
import { combineReducers } from "redux";
import todos from "./todos";
import filter from "./filter";
import users from "./users";
const reducer = combineReducers({
todos,
filter,
users,
});
export default reducer;
// ./containers/UserListContainer.jsx
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import UserList from "../components/UserList";
import { getUsersFali, getUsersStart, getUsersSuccess } from "../redux/actions";
import axios from "axios";
export default function UserListContainer() {
const users = useSelector((state) => state.users.data);
const dispatch = useDispatch();
const getUsers = useCallback(async () => {
try {
dispatch(getUsersStart());
const res = await axios.get("https://api.github.com/users");
dispatch(getUsersSuccess(res.data));
} catch (error) {
dispatch(getUsersFali(error));
}
}, [dispatch]);
return <UserList users={users} getUsers={getUsers} />;
}
// ./components/UserList.jsx
import { useEffect } from "react";
export default function UserList({ users, getUsers }) {
useEffect(() => {
getUsers();
}, [getUsers]);
if (users.length === 0) {
return <p>νμ¬ μ μ μ 보 μμ</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.login}</li>
))}
</ul>
);
}
π 리λμ€ λ―Έλ€μ¨μ΄
https://redux.js.org/tutorials/fundamentals/part-4-store#middleware
- λ―Έλ€μ¨μ΄κ° "λμ€ν¨μΉ" μ μλ€μ μ½λλ₯Ό μΆκ°ν μ μκ² ν΄μ€λλ€.
- λ―Έλ€μ¨μ΄κ° μ¬λ¬κ°λ©΄ λ―Έλ€μ¨μ΄κ° "μμ°¨μ μΌλ‘" μ€νλ©λλ€.
- λ λ¨κ³κ° μμ΅λλ€.
- μ€ν μ΄λ₯Ό λ§λ€λ, λ―Έλ€μ¨μ΄λ₯Ό μ€μ νλ λΆλΆ
- {createStore, applyMiddleware} from redux
- λμ€ν¨μΉκ° νΈμΆλ λ μ€μ λ‘ λ―Έλ€μ¨μ΄λ₯Ό ν΅κ³Όνλ λΆλΆ
- μ€ν μ΄λ₯Ό λ§λ€λ, λ―Έλ€μ¨μ΄λ₯Ό μ€μ νλ λΆλΆ
- βdispatch λ©μλλ₯Ό ν΅ν΄ storeλ‘ κ°κ³ μλ μ‘μ μ κ°λ‘μ±λ μ½λ
import { applyMiddleware } from "redux";
// ./redux/stroe.js
import { applyMiddleware, createStore } from "redux";
import reducer from "./reducers/reducer";
function middleware1(store) {
console.log("middleware1", 0);
return (next) => {
console.log("middleware1", 1, next);
return (action) => {
console.log("middleware1", 2);
const returnValue = next(action);
console.log("middleware1", 3);
return returnValue;
};
};
}
function middleware2(store) {
console.log("middleware2", 0);
return (next) => {
console.log("middleware2", 1, next);
return (action) => {
console.log("middleware2", 2);
const returnValue = next(action);
console.log("middleware2", 3);
return returnValue;
};
};
}
const store = createStore(reducer, applyMiddleware(middleware1, middleware2));
export default store;
π redux-devtools
$ npm i redux-devtools-extension -D
import { composeWithDevTools } from "redux-devtools-extension";
// ./redux/store.js
import { applyMiddleware, createStore } from "redux";
import reducer from "./reducers/reducer";
import { composeWithDevTools } from "redux-devtools-extension";
const store = createStore(reducer, composeWithDevTools(applyMiddleware()));
export default store;
π redux-thunk β
https://github.com/reduxjs/redux-thunk
- 리λμ€ λ―Έλ€μ¨μ΄
- 리λμ€λ₯Ό λ§λ μ¬λμ΄ λ§λ€μμ. (Dan)
- 리λμ€μμ λΉλκΈ° μ²λ¦¬λ₯Ό μν λΌμ΄λΈλ¬λ¦¬
- μ‘μ μμ±μλ₯Ό νμ©νμ¬ λΉλκΈ° μ²λ¦¬
- μ‘μ μμ±μκ° μ‘μ μ 리ν΄νμ§ μκ³ , ν¨μλ₯Ό 리ν΄ν¨.
$ npm i redux-thunk
import thunk from "redux-thunk";
// ./redux/store.js
import { applyMiddleware, createStore } from "redux";
import reducer from "./reducers/reducer";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
const store = createStore(reducer, composeWithDevTools(applyMiddleware(thunk)));
export default store;
// ./redux/action.js
import axios from "axios";
...
export function getUsersThunk() {
return async (dispatch) => {
try {
dispatch(getUsersStart());
const res = await axios.get("https://api.github.com/users");
dispatch(getUsersSuccess(res.data));
} catch (error) {
dispatch(getUsersFali(error));
}
};
}
// ./container/UserListContainer.jsx
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import UserList from "../components/UserList";
import { getUsersThunk } from "../redux/actions";
export default function UserListContainer() {
const users = useSelector((state) => state.users.data);
const dispatch = useDispatch();
// const getUsers = useCallback(async () => {
// try {
// dispatch(getUsersStart());
// const res = await axios.get("https://api.github.com/users");
// dispatch(getUsersSuccess(res.data));
// } catch (error) {
// dispatch(getUsersFali(error));
// }
// }, [dispatch]);
const getUsers = useCallback(() => {
dispatch(getUsersThunk());
}, [dispatch]);
return <UserList users={users} getUsers={getUsers} />;
}
π redux-promise-middleware
https://github.com/pburtchaell/redux-promise-middleware
$ npm i redux-promise-middleware
import promise from "redux-promise-middleware";
// ./redux/store.js
import { applyMiddleware, createStore } from "redux";
import reducer from "./reducers/reducer";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
const store = createStore(
reducer,
composeWithDevTools(applyMiddleware(thunk, promise))
);
export default store;
// ./reudx/action.js
import axios from "axios";
...
const GET_USERS = "GET_USERS";
export const GET_USERS_PENDING = "GET_USERS_PENDING";
export const GET_USERS_FULFILLED = "GET_USERS_FULFILLED";
export const GET_USERS_REJECTED = "GET_USERS_REJECTED";
export function getUsersPromise() {
return {
type: GET_USERS,
payload: async () => {
const res = await axios.get("https://api.github.com/users");
return res.data;
},
};
}
// ./redux/users.js
import {
GET_USERS_FAIL,
GET_USERS_FULFILLED,
GET_USERS_PENDING,
GET_USERS_REJECTED,
GET_USERS_START,
GET_USERS_SUCCESS,
} from "../actions";
const initialState = { loading: false, data: [], error: null };
export default function users(state = initialState, action) {
if (action.type === GET_USERS_START || action.type === GET_USERS_PENDING) {
return {
...state,
loading: true,
error: null,
};
}
if (action.type === GET_USERS_SUCCESS) {
return {
...state,
loading: false,
data: action.data,
};
}
if (action.type === GET_USERS_FULFILLED) {
return {
...state,
loading: false,
data: action.payload,
};
}
if (action.type === GET_USERS_FAIL) {
return {
...state,
loading: false,
error: action.error,
};
}
if (action.type === GET_USERS_REJECTED) {
return {
...state,
loading: false,
error: action.payload,
};
}
return state;
}
// ./containers/UserListContainer.jsx
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import UserList from "../components/UserList";
import { getUsersPromise, getUsersThunk } from "../redux/actions";
export default function UserListContainer() {
const users = useSelector((state) => state.users.data);
const dispatch = useDispatch();
// const getUsers = useCallback(async () => {
// try {
// dispatch(getUsersStart());
// const res = await axios.get("https://api.github.com/users");
// dispatch(getUsersSuccess(res.data));
// } catch (error) {
// dispatch(getUsersFali(error));
// }
// }, [dispatch]);
const getUsers = useCallback(() => {
dispatch(getUsersPromise());
}, [dispatch]);
return <UserList users={users} getUsers={getUsers} />;
}
π Ducks Pattern β
https://github.com/erikras/ducks-modular-redux
κ·μΉ
νλμ λͺ¨λμ...
- νμ reducer()λ μ΄λ¦μ ν¨μλ₯Ό export default ν΄μΌν©λλ€.
- νμ λͺ¨λμ action μμ±μλ€μ ν¨μννλ‘ export ν΄μΌν©λλ€.
- νμ npm-module-or-app/reducer/ACTION_TYPE ννμ action νμ μ κ°μ ΈμΌν©λλ€.
- μ΄μ©λ©΄ action νμ λ€μ UPPER_SNAKE_CASEλ‘ export ν μ μμ΅λλ€. λ§μ½, μΈλΆ reducerκ° ν΄λΉ actionλ€μ΄ λ°μνλμ§ κ³μ κΈ°λ€λ¦¬κ±°λ, μ¬μ¬μ©ν μ μλ λΌμ΄λΈλ¬λ¦¬λ‘ νΌλΈλ¦¬μ±ν κ²½μ°μ λ§μ΄μ£ .
μ¬μ¬μ©κ°λ₯ν Redux λΌμ΄λΈλ¬λ¦¬ ννλ‘ κ³΅μ νλ {actionType, action, reducer} λ¬Άμμλ μ κ·μΉμ μΆμ²ν©λλ€.
// ./redux/modules/todos.js
// μ‘μ
νμ
μ μ
export const ADD_TODO = "redux-start/todos/ADD_TODO";
export const COMPLETE_TODO = "redux-start/todos/COMPLETE_TODO";
// μ‘μ
μμ± ν¨μ
// {type: ADD_TODO, text: 'ν μΌ'}
export function addTodo(text) {
return {
type: ADD_TODO,
text,
};
}
// {type: COMPLETE_TODO, index: 3}
export function completoTodo(index) {
return {
type: COMPLETE_TODO,
index,
};
}
// μ΄κΈ°κ°
const initialState = [];
// 리λμ
export default function reducer(previousState = initialState, action) {
...
}
// ./redux/modules/filter.js
// μ‘μ
νμ
μ μ
const SHOW_ALL = "redux-start/filter/SHOW_ALL";
const SHOW_COMPLETE = "redux-start/filter/SHOW_COMPLETE";
// μ‘μ
μμ± ν¨μ
export function showAll() {
return { type: SHOW_ALL };
}
export function showComplete() {
return { type: SHOW_COMPLETE };
}
// μ΄κΈ°κ°
const initialState = "ALL";
// 리λμ
export default function reducer(previousState = initialState, action) {
...
}
// ./modules/users.js
import axios from "axios";
// μ‘μ
νμ
μ μ
// κΉν API νΈμΆμ μμνλ κ²μ μλ―Έ
export const GET_USERS_START = "redux-start/users/GET_USERS_START";
// κΉν API νΈμΆμ λν μλ΅μ΄ μ±κ³΅μ μΌλ‘ λμμ¨ κ²½μ°
export const GET_USERS_SUCCESS = "redux-start/users/GET_USERS_SUCCESS";
// κΉν API νΈμΆμ λν μλ΅μ΄ μ€ν¨ν κ²½μ°
export const GET_USERS_FAIL = "redux-start/users/GET_USERS_FAIL";
// redux-promise-middleware
const GET_USERS = "redux-start/users/GET_USERS";
export const GET_USERS_PENDING = "redux-start/users/GET_USERS_PENDING";
export const GET_USERS_FULFILLED = "redux-start/users/GET_USERS_FULFILLED";
export const GET_USERS_REJECTED = "redux-start/users/GET_USERS_REJECTED";
// μ‘μ
μμ± ν¨μ
export function getUsersStart() {
return {
type: GET_USERS_START,
};
}
export function getUsersSuccess(data) {
return {
type: GET_USERS_SUCCESS,
data,
};
}
export function getUsersFali(error) {
return {
type: GET_USERS_FAIL,
error,
};
}
// μ΄κΈ°κ°
const initialState = { loading: false, data: [], error: null };
// 리λμ
export default function reducer(state = initialState, action) {
...
}
// react-thunk
export function getUsersThunk() {
return async (dispatch) => {
try {
dispatch(getUsersStart());
const res = await axios.get("https://api.github.com/users");
dispatch(getUsersSuccess(res.data));
} catch (error) {
dispatch(getUsersFali(error));
}
};
}
// redux-promise-middleware
export function getUsersPromise() {
return {
type: GET_USERS,
payload: async () => {
const res = await axios.get("https://api.github.com/users");
return res.data;
},
};
}
// ./redux/modules/reducer.js
import { combineReducers } from "redux";
import todos from "./todos";
import filter from "./filter";
import users from "./users";
const reducer = combineReducers({
todos,
filter,
users,
});
export default reducer;
π react-router-dom κ³Ό redux ν¨κ» μ°κΈ°
https://github.com/supasate/connected-react-router
$ npm i react-router-dom
// ./App.js
import "./App.css";
import { BrowserRouter, Route, Router, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Todos from "./pages/Todos";
import Users from "./pages/Users";
import history from "./history";
function App() {
return (
<Router history={history}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
<Route path="/users" element={<Users />} />
</Routes>
</Router>
);
}
export default App;
// ./pages/Home.jsx
import { Link } from "react-router-dom";
export default function Home() {
return (
<div>
<h1>Home</h1>
<ul>
<li>
<Link to="/todos">Todos</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
</div>
);
}
// ./pages/Todos.jsx
import TodoListContainer from "../containers/TodoListContainer";
import TodoFormContainer from "../containers/TodoFormContainer";
export default function Todos() {
return (
<div>
<TodoListContainer />
<TodoFormContainer />
</div>
);
}
// ./pages/Users.jsx
import UserListContainer from "../containers/UserListContainer";
export default function Users() {
return (
<div>
<UserListContainer />
</div>
);
}
// history.js
import { createBrowserHistory } from "history";
const history = createBrowserHistory();
export default history;
// ./redux/store.js
import { applyMiddleware, createStore } from "redux";
import reducer from "./modules/reducer";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
import history from "../history";
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(thunk.withExtraArgument({ history }), promise)
)
);
export default store;
// ./modules/users.js
...
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
// react-thunk
export function getUsersThunk() {
return async (dispatch, getState, { history }) => {
try {
console.log(history);
dispatch(getUsersStart());
const res = await axios.get("https://api.github.com/users");
dispatch(getUsersSuccess(res.data));
// sleep
await sleep(2000);
history.push("/");
} catch (error) {
dispatch(getUsersFali(error));
}
};
}
// redux-promise-middleware
...
}
- κ°νκ² μ°κ²°
$ npm i connected-react-router
// ./App.js
import "./App.css";
import { BrowserRouter, Route, Router, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Todos from "./pages/Todos";
import Users from "./pages/Users";
import history from "./history";
import { ConnetedRouter } from "connected-react-router";
function App() {
return (
<ConnetedRouter history={history}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
<Route path="/users" element={<Users />} />
</Routes>
</ConnetedRouter>
);
}
export default App;
// ./redux/reducer.js
import { combineReducers } from "redux";
import todos from "./todos";
import filter from "./filter";
import users from "./users";
import { connectRouter } from "conneted-react-router";
import history from "../../history";
const reducer = combineReducers({
todos,
filter,
users,
router: connectRouter(history),
});
export default reducer;
// ./redux/store.js
import { applyMiddleware, createStore } from "redux";
import reducer from "./modules/reducer";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
import history from "../history";
import { routerMiddleware } from "conneted-react-router";
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(
thunk.withExtraArgument({ history }),
promise,
routerMiddleware(history)
)
)
);
export default store;
// ./pages/Home.jsx
import { useDispatch } from "react-redux";
import { Link } from "react-router-dom";
import { push } from "connected-react-router";
export default function Home() {
const dispatch = useDispatch();
return (
<div>
<h1>Home</h1>
<ul>
<li>
<Link to="/todos">Todos</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
<button onClick={click}>todo λ‘ μ΄λ</button>
</div>
);
function click() {
dispatch(push("/todos"));
}
}
π redux-saga : μ¬μ΄λ μ΄ννΈ(λΉλκΈ° λ‘μ§)λ₯Ό 컨νΈλ‘€ ν μ μλ€.
- λ―Έλ€μ¨μ΄ μ λλ€.
- μ λλ μ΄ν° κ°μ²΄λ₯Ό λ§λ€μ΄ λ΄λ μ λ€λ μ΄ν° μμ± ν¨μλ₯Ό μ΄μ©ν©λλ€.β
- 리λμ€ μ¬κ° λ―Έλ€μ¨μ΄λ₯Ό μ€μ νκ³ ,
- λ΄κ° λ§λ μ¬κ° ν¨μλ₯Ό λ±λ‘ν ν
- μ¬κ° λ―Έλ€μ¨μ΄λ₯Ό μ€νν©λλ€.
- κ·Έλ¦¬κ³ λ±λ‘λ μ¬κ° ν¨μλ₯Ό μ€νν μ‘μ μ λμ€ν¨μΉνλ©΄ λ©λλ€.
$ npm i redux-saga
// ./redux/modules/users.js
import axios from "axios";
import { call, delay, put, takeEvery } from "redux-saga/effects";
import { push } from "connected-react-router";
...
// redux-saga
const GET_USERS_SAGA_START = "GET_USERS_SAGA_START";
// β 리λμ€ μ¬κ° λ―Έλ€μ¨μ΄λ₯Ό μ€μ νκ³ ,
function* getUsersSaga(action) {
try {
yield put(getUsersStart);
const res = yield call(axios.get, "https://api.github.com/users");
yield put(getUsersSuccess(res.data));
// sleep
yield delay(2000);
// history.push("/");
yield put(push("/"));
} catch (error) {
yield put(getUsersFali(error));
}
}
export function getUsersSagaStart() {
return {
type: GET_USERS_SAGA_START,
};
}
// β λ΄κ° λ§λ μ¬κ° ν¨μλ₯Ό λ±λ‘ν ν
export function* usersSaga() {
yield takeEvery(GET_USERS_SAGA_START, getUsersSaga);
}
// ./redux/modules/rootSaga.js
import { all } from "redux-saga/effects";
import { usersSaga } from "./users";
export default function* rootSaga() {
yield all([usersSaga()]);
}
// ./redux/store.js
import { applyMiddleware, createStore } from "redux";
import reducer from "./modules/reducer";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
import history from "../history";
import { routerMiddleware } from "conneted-react-router";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./modules/rootSaga";
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(
thunk.withExtraArgument({ history }),
promise,
routerMiddleware(history),
sagaMiddleware
)
)
);
// β μ¬κ° λ―Έλ€μ¨μ΄λ₯Ό μ€νν©λλ€.
sagaMiddleware.run(rootSaga);
export default store;
// ./containers/UserListContainer.jsx
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import UserList from "../components/UserList";
import { getUsersSagaStart, getUsersThunk } from "../redux/modules/users";
export default function UserListContainer() {
const users = useSelector((state) => state.users.data);
const dispatch = useDispatch();
// const getUsers = useCallback(async () => {
// try {
// dispatch(getUsersStart());
// const res = await axios.get("https://api.github.com/users");
// dispatch(getUsersSuccess(res.data));
// } catch (error) {
// dispatch(getUsersFali(error));
// }
// }, [dispatch]);
// β κ·Έλ¦¬κ³ λ±λ‘λ μ¬κ° ν¨μλ₯Ό μ€νν μ‘μ
μ λμ€ν¨μΉνλ©΄ λ©λλ€.
const getUsers = useCallback(() => {
dispatch(getUsersSagaStart());
}, [dispatch]);
return <UserList users={users} getUsers={getUsers} />;
}
π redux-actions : Ducks Pattern μ μ½κ² ꡬνν μ μλλ‘ λμμ£Όλ λΌμ΄λΈλ¬λ¦¬
https://github.com/redux-utilities/redux-actions
https://redux-actions.js.org/api
$ npm i redux-actions
import { createActions, handleActions } from "redux-actions";
// μ‘μ
νμ
μ μ + μ‘μ
μμ± ν¨μ
export const { showAll, showComplete } = createActions(
"SHOW_ALL",
"SHOW_COMPLETE",
{
prefix: "redux-start/filter",
}
);
// μ΄κΈ°κ°
const initialState = "ALL";
// 리λμ
const reducer = handleActions(
{
SHOW_ALL: (state, actions) => "ALL",
SHOW_COMPLETE: (state, actions) => "COMPLETE",
},
initialState,
{ prefix: "redux-start-filter" }
);
export default reducer;
'π¬ > γ γ γ γ γ γ μ±λ¦°μ§' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
45μΌμ°¨ (0) | 2022.03.28 |
---|---|
43, 44μΌμ°¨ (0) | 2022.03.26 |
41μΌμ°¨ (0) | 2022.03.25 |
useMemo μ useCallback μ 무μμ΄ λ€λ₯Έ κ²μΈκ°...... (0) | 2022.03.25 |
40μΌμ°¨ (0) | 2022.03.24 |