Skip to content

Latest commit

 

History

History
299 lines (221 loc) · 6.89 KB

reduxSaga.md

File metadata and controls

299 lines (221 loc) · 6.89 KB

redux-saga

Setup

  • files changes example

  • 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 data

    import 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 dispatch

    import { 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 on type

    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 store

    import { 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-saga

    import { 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

Loading

Tips

  • dispatch another saga event in saga, use yield call(eventFunction) instead of yield 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;
        }
    }
}