redux-saga
Setup
-
install packages
yarn add react-redux redux-saga immer
- redux : global state
- reduex-saga : middleware is used for async request
- immer : work with immutable state in a more convenient way
-
8 files you need to setup for this tutorial
. └── config | └── configureStore.js └── constants | └── demo.js └── pages | └── _app.js | └── demo-redux-saga.js (optional it's for demo) └── redux ├── actions | └── demo.js └── reducers | └── demo.js | └── todo.js └── sagas └── demo.js └── todo.js
-
constant
./constants/demo.js
// dispatch to saga from hook export const ACTION_DEMO_ADD = "ACTION_DEMO_ADD"; // dispatch to redux from saga 'put' effect export const ACTION_DEMO_ADD_SUC = "ACTION_DEMO_ADD_SUC";
-
redux-saga
./redux/sagas/demo.js
: watch saga dispacth.import { takeEvery, put, select } from "redux-saga/effects"; import { ACTION_DEMO_ADD, ACTION_DEMO_ADD_SUC } from "../../constants/demo"; let counter = 0; function* add({ text }) { const { items } = yield select(state => state.demo) yield put({ type: ACTION_DEMO_ADD_SUC, payload: [...items, { text, id: counter++ }] }) } export default [ takeEvery(ACTION_DEMO_ADD, add) ]
./redux/sagas/index.js
: export all sagas to watch at the same time.import { all } from "redux-saga/effects" import demo from "./demo" // remove this in your app export default function* rootSaga() { yield all([...demo]) }
-
reducer
./redux/reducers/demo.js
: watch saga's ‘put' effect to call reducer with preprocessed dataimport produce from "immer"; import { ACTION_DEMO_ADD_SUC } from "../../constants/demo"; const initialState = { items: [] } const todo = (state = initialState, action) => produce(state, draft => { switch (action.type) { case ACTION_DEMO_ADD_SUC: draft.items = action.payload; break default: break } }) export default todo;
./redux/reducers/index.js
: watch all actions includes sagas' put effect and original redux dispatchimport { combineReducers } from "redux"; import demo from "./demo" const rootReducer = combineReducers({ demo }) export default rootReducer;
-
action
./redux/actions/demo.js
: funtion use to dispatch saga or redux depends ontype
import { ACTION_DEMO_ADD } from "../../constants/demo"; export const demo = (text) => { return { type: ACTION_DEMO_ADD, text } }
-
store with saga
./config/configureStore.js
import { createStore, applyMiddleware } from "redux"; import createSagaMiddleware from "redux-saga"; import rootReducer from "../redux/reducers"; import rootSaga from "../redux/sagas"; const sagaMiddleware = createSagaMiddleware(); export default createStore(rootReducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(rootSaga);
-
use store in app
./pages/_app.js
: use provider with storeimport { Provider } from "react-redux"; import store from "../config/configureStore"; function MyApp({ Component, pageProps }) { return ( <Provider store={store}> <Component {...pageProps} /> </Provider> ) } export default MyApp
-
add page for testing
pages/demo-redux-saga.js
and visit http://localhost:3000/demo-redux-sagaimport { useState } from "react" import { useSelector, useDispatch } from "react-redux" import { demo } from "../redux/actions/demo" const TextInput = () => { const [text, setText] = useState("") const items = useSelector(state => state.demo) const dispatch = useDispatch() const onChange = (e) => { setText(e.target.value) } const onClick = () => { dispatch(demo(text)) setText("") } return <> <pre>{JSON.stringify(items, null, 4)}</pre> <input type="text" value={text} onChange={onChange} /> <button onClick={onClick}>add</button> </> } export default function App(){ return <TextInput/> }
Flow chart of Load data
sequenceDiagram
Note left of UI: onClick
par Call Reducer
UI->>Redux: 1.dispatch(ACTION_LOAD_DATA)
and Call Redux-saga
UI->>Redux-saga: 1.dispatch(ACTION_LOAD_DATA)
end
Redux->>UI: 2.loading=true
opt Need Current State
Redux-saga-->>+Redux: 3.yield select(state=>state)
Redux-->>-Redux-saga: 4.data
end
Redux-saga->>+Server: 5.yield call(api)
Server-->>-Redux-saga: 6.response
alt SUCCESS
Redux-saga->>Redux: 7.yield put(ACTION_LOAD_DATA_SUCCESS)
Redux->>UI: 8.loading=false, data
else FAIL
Redux-saga->>Redux: 7.yield put(ACTION_LOAD_DATA_FAIL)
Redux->>UI: 8.loading=false, error message
end
Tips
- dispatch another saga event in saga, use
yield call(eventFunction)
instead ofyield put({type:EVENT_CONSTANT})
Advanced
WebSocket
import { take, call, race } from "redux-saga/effects";
import { eventChannel, END } from "redux-saga";
function initWebsocketChannel(socketUrl) {
return eventChannel((emitter) => {
const socket = new WebSocket(socketUrl);
socket.onopen = () => {
emitter({type:SOCKET_CONNECTED,payload:{}});
};
socket.onclose = () => {
emitter({type:SOCKET_CLOSED,payload:{}});
emitter(END);
};
socket.onmessage = (payload) => {
emitter({type:SOCKET_MSG,payload});
};
return () => socket.close();
});
}
function* initWebSocket() {
const socketUrl="wss://..."
const channel = yield call(initWebsocketChannel, socketUrl);
while (true) {
const action = yield race([take(channel), take([CLOSE_SOCKET])]);
const socketAction = action[0];
if (socketAction) {
yield put(socketAction);
}
const closeAction = action[1];
if (closeAction) {
channel.close();
break;
}
}
}