使用Redux管理異步數據

簡介

Redux 相關知識參考:Redux基本使用

Redux 中的 reducer 只能處理同步

如果需要用 Redux 處理異步請求,可以使用異步相關的插件。

一般有兩個步驟:1、異步請求接口;2、將請求結果存到 store 中

比較常用的有:redux-thunkredux-saga

redux-thunk 使用

  • 安裝: npm i redux-thunk --save

  • 使用 applyMiddleware 添加中間件

  • 使用 compose 合并中間件和 redux-devtools (只有這個中間件的話,就沒必要用這個方法了)

  • 修改 src/store/index.js 文件

import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'

export default createStore(rootReducer, compose(
  applyMiddleware(thunk),
  window.devToolsExtension ? window.devToolsExtension() : f=>f
))
  • 修改 src/store/action.js,添加異步方法
// ...
export const testCountAddAsync = num => {
  return dispatch => {
    setTimeout(() => {
      dispatch(testCountAdd(num))
    }, 2000)
  }
}
// ...
  • 在組件中使用
@connect(
  ({ test }) => ({ count: test.count}),
  { testCountAddAsync }
)
class Demo extends React.Component {
  // ...
}
export default Demo

redux-saga

使用的是 ES6 的 generator 語法。該語法知識參考:一文掌握生成器 Generator,利用 Generator 實現異步編程

備注
下面示例中的代碼,使用 redux-actions 管理 actions。
相關使用參考:使用redux-actions優化actions管理

  • 安裝: npm i redux-saga --save

  • 修改 src/store/actions.js 文件,添加 action

import { createActions } from 'redux-actions'

// 建議區分開 action 的用途,方便后期維護
// ON_XXX 開頭的給 saga 用
// 其他的,比如 SET_XXX、ClEAR_XXX 等給 reducer 用
export default createActions({
  // ...
  DEMO: {
    ON_USER_INFO: undefined,
    SET_USER_INFO: undefined,
  }
})
  • src/store/reducer/ 目錄下新建 demo.js 文件
import { handleActions } from 'redux-actions'
import Actions from '../actions'

const INITIAL_STATE = {
  userInfo: null,
}

export default handleActions({
  [Actions.demo.setUserInfo](state, { payload }) {
    return {
      ...state,
      userInfo: payload
    }
  },
}, INITIAL_STATE)
  • 修改 src/store/reducer/index.js 文件,添加 demo
import { combineReducers } from 'redux'
// ...
import demo from './demo'

export default combineReducers({
  // ...
  demo,
})
  • src/store/ 目錄下添加 saga/ 目錄

  • src/store/saga/ 目錄下添加 demo.js 文件

import { all, takeLatest, put } from 'redux-saga/effects'
import Actions from '../actions'

// 延時,用來模擬請求
export const delay = (time) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, time);
  });
};
// 模擬請求
const request = async (url, options) => {
  console.log(url, options)
  await delay(1000)
  return {
    code: 0,
    data: {
      name: 'wmm66',
      age: 18,
    }
  }
}

export function* fetchUserInfo({ payload }) {
  const { id } = payload || {}
  const res = yield request('user/info', { id })
  if (res && res.code === 0) {
    yield put(Actions.demo.setUserInfo(res.data))
  }
}

export default all([
  takeLatest(Actions.demo.onUserInfo, fetchUserInfo),
])
  • src/store/saga/ 目錄下添加 index.js 文件
import { all } from 'redux-saga/effects'
import demoSaga from './demo'

export default function* rootSage() {
  yield all([
    demoSaga
  ])
}
  • 修改 src/store/index.js 文件
import { createStore, applyMiddleware, compose } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer from './reducer'
import rootSaga from './saga'

const sagaMiddleware = createSagaMiddleware()
const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
    // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
  }) : compose;

const middlewares = [sagaMiddleware];
// if (process.env.NODE_ENV === 'development') {
//   middlewares.push(require('redux-logger').createLogger());
// }
const enhancer = composeEnhancers(applyMiddleware(...middlewares))

const store = createStore(rootReducer, enhancer);
sagaMiddleware.run(rootSaga);
export default store
export { default as Actions } from './actions'
  • 頁面中使用
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import Actions from '@/store/actions'

export default function Test(props) {
  const { userInfo } = useSelector(({ demo }) => demo)
  const dispatch = useDispatch()

  const onFetchUser = () => {
    dispatch(Actions.demo.onUserInfo({ id: 123 }))
  }

  return (
    <div>
      <button onClick={onFetchUser}>請求數據</button>
      <div>{JSON.stringify(userInfo)}</div>
    </div>
  )
}

擴展

有的時候,我們需要在某些異步操作結束后(比如接口請求完成后)做某些操作

使用 Promise 的時候,我們可以使用 then、async/await 等很方便的做到

generator 語法可以使用 callback 的方式實現

  • 基本代碼如下:
// src/store/saga/demo.js文件
export function* fetchUserInfo({ payload }) {
  const { id, callback } = payload || {}
  const res = yield request('user/info', { id })
  if (res && res.code === 0) {
    yield put(Actions.demo.setUserInfo(res.data))
    callback && callback(res.data)
  }
}
// 使用
const onFetchUser = () => {
  dispatch(Actions.demo.onUserInfo({ id: 123, callback: (userInfo) => {
    console.log(userInfo)
  } }))
}
  • 我們可以將其 Promise 化,方便使用
const promisifySaga = () => {
  return new Promise((resolve, reject) => {
    dispatch(Actions.demo.onUserInfo({ id: 123, callback: (userInfo) => {
      resolve(userInfo)
    } }))
  })
}

const onFetchUser = async () => {
  const userInfo = await promisifySaga()
  console.log(userInfo)
}
  • promisifySaga 應該是一個比較通用的方法,我們通過傳參來實現
function promisifySaga({dispatch, action, params = {}}) {
  return new Promise((resolve, reject) => {
    if (!dispatch || !action || typeof action !== 'function') {
      reject('參數錯誤')
      return
    }
    dispatch(action({ ...params, callback: (res) => {
      resolve(res)
    } }))
  })
}

const onFetchUser = async () => {
  const userInfo = await promisifySaga({
    dispatch,
    action: Actions.demo.onUserInfo,
    params: { id: 123 }
  })
  console.log(userInfo)
}
  • promisifySaga 的意思是將 saga 給 promise 化,我們希望這么調用 promisifySaga({dispatch, action})({ id: 123 })
function promisifySaga({dispatch, action}) {
  return (params = {}) => {
    return new Promise((resolve, reject) => {
      if (!dispatch || !action || typeof action !== 'function') {
        reject('參數錯誤')
        return
      }
      dispatch(action({ ...params, callback: (res) => {
        resolve(res)
      } }))
    })
  }
}

const onFetchUser = async () => {
  const userInfo = await promisifySaga({
    dispatch,
    action: Actions.demo.onUserInfo
  })({ id: 123 })
  console.log(userInfo)
}
  • 最終代碼如下
// src/store/saga/demo.js
export function* fetchUserInfo({ payload }) {
  const {
    id,
    callback = i => i,
  } = payload || {}
  // 這里推薦捕捉錯誤,往后拋
  try {
    const res = yield request('user/info', { id })
    if (res && res.code === 0) {
      yield put(Actions.demo.setUserInfo(res.data))
      callback({ status: 'success', data: res.data })
    } else {
      callback({ status: 'error', reason: res.msg })
    }
  } catch(err) {
    callback({ status: 'error', reason: err })
  }
}
// src/lib/utils.js
import store from '@/store'
/**
 * 是通過回調函數實現的
 * 如果想在異步執行后執行某些操作,對應的saga必須設置callback
 * 成功:callback({ status: 'success', data: res.data })
 * 失敗:callback({ status: 'error', reason: err })
 */
export const promisifySaga = action => {
  return (params = {}) => {
    return new Promise((resolve, reject) => {
      if (!action || typeof action !== 'function') {
        reject('參數錯誤')
        return
      }
      store.dispatch(action({
        ...params,
        callback: ({ status, data, reason }) => {
          status === 'success'
            ? resolve(data)
            : reject(reason)
        },
      }))
    })
  }
}
// 代碼中使用
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import Actions from '@/store/actions'
import { promisifySaga } from '@/lib/utils'

export default function Test(props) {
  const { userInfo } = useSelector(({ demo }) => demo)
  const dispatch = useDispatch()

  const onFetchUser = () => {
    const userInfo = await promisifySaga(Actions.demo.onUserInfo)({ id: 123 })
    console.log(userInfo)
  }

  return (
    <div>
      <button onClick={onFetchUser}>請求數據</button>
      <div>{JSON.stringify(userInfo)}</div>
    </div>
  )
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容