翻譯|Where and When to Fetch Data With Redux

phpsmarter發表於2019-04-28

原文:Where and When to Fetch Data With Redux

如果韌體為了渲染需要一些資料,你想使用Redux獲取資料,並儲存在Redux Store中,那麼什麼時間點是呼叫API的最好時機?

  • componentdidMount生命週期函式中啟動Action

在Redux中呼叫API

假設你要顯示一個產品列表. 後臺API是:'GET/products',可以這麼建立Redux action

productAction.js

export function fetchProducts() {
  return dispatch => {
    dispatch(fetchProductsBegin());
    return fetch("/products")
      .then(handleErrors)
      .then(res => res.json())
      .then(json => {
        dispatch(fetchProductsSuccess(json.products));
        return json.products;
      })
      .catch(error => dispatch(fetchProductsFailure(error)));
  };
}

// Handle HTTP errors since fetch won't.
function handleErrors(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }
  return response;
}
複製程式碼

註釋:fetch()不會丟擲HTTP error,例如404錯誤. 這一點讓人有點困惑,如果你之前使用的是其他的方法,例如axios. 看這裡,有關於fetch和錯誤處理的內容.

在Redux中,使用redux-thunk獲取資料 通常,actions必須是一個簡單物件.返回一個函式,例如例項中的fetchProducts,超出了範圍,Redux不允許這麼做.至少在沒有協助的情況下不行. 所以redux-thunk就出現了.redux-thunk是一箇中介軟體可以告訴Redux如何處理新型別的action(如果很好奇,可以看看thunk到底是什麼東東?)

等等.神馬情況? redux-thunk,Reducers有些意義. redux-thunk是完全捏造出來的吧?

如果你處在對Redux似懂非懂的邊緣,要大膽大往前嘗試,儘管可能不太明白到底是怎麼一回事.我會把這件事說明白.

即使你已經搞清楚了,或許理解的很透徹. 回顧一下也是值得的.

使用npm install redux-thunk安裝redux-thunk.接著需要新增幾行程式碼擴充套件Redux store,以便使用新的中介軟體

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);
複製程式碼

重要的一件事要注意,在傳遞給Redux之前,必須要用applyMiddleware包裝中介軟體. 這裡還出現了rootReducer,之後我們會看看它的出處.

這段程式碼可以解除安裝index.js中,或者寫在自己的檔案中(store.js是個很好的名字).Redux不關心檔案放在那裡.如果你願意,放在一起也可以.只要能夠獲取到store,並通過Provider提供給app就設定完成了.

如何命名 Redux Actions

Redux action獲取資料通常是三部曲:BEGIN,SUCCESS,FAULURE,這不是必須的,只是約定俗成.

在起始API呼叫之前,dispatch BEGIN action api呼叫成功之後, dispatch SUCCESS和資料.如果api呼叫失敗,dispatch FAILURE和error.

有時候, 最後一次的呼叫用ERROR代替.不太理想,只是為了統一.

BEGIN/SUCCESS/FAILURE 模式很好,因為他給出了一個掛鉤用於追蹤具體發生的事-例如,設定 "loading"標誌為true標識BEGIN action, SUCCESS或者FAILURE時設定為false.下面是action的樣子:

productActions.js

export const FETCH_PRODUCTS_BEGIN   = 'FETCH_PRODUCTS_BEGIN';
export const FETCH_PRODUCTS_SUCCESS = 'FETCH_PRODUCTS_SUCCESS';
export const FETCH_PRODUCTS_FAILURE = 'FETCH_PRODUCTS_FAILURE';

export const fetchProductsBegin = () => ({
  type: FETCH_PRODUCTS_BEGIN
});

export const fetchProductsSuccess = products => ({
  type: FETCH_PRODUCTS_SUCCESS,
  payload: { products }
});

export const fetchProductsFailure = error => ({
  type: FETCH_PRODUCTS_FAILURE,
  payload: { error }
});
複製程式碼

之後,在收到FETCH_PRODUCTS_SUCCESS action時用reducer儲存products至Redux store. 也同樣在獲取資料開始時,設定loading標誌為true,完成或失敗時設定為false.

productReducer.js

import {
  FETCH_PRODUCTS_BEGIN,
  FETCH_PRODUCTS_SUCCESS,
  FETCH_PRODUCTS_FAILURE
} from './productActions';

const initialState = {
  items: [],
  loading: false,
  error: null
};

export default function productReducer(state = initialState, action) {
  switch(action.type) {
    case FETCH_PRODUCTS_BEGIN:
      // Mark the state as "loading" so we can show a spinner or something
      // Also, reset any errors. We're starting fresh.
      return {
        ...state,
        loading: true,
        error: null
      };

    case FETCH_PRODUCTS_SUCCESS:
      // All done: set loading "false".
      // Also, replace the items with the ones from the server
      return {
        ...state,
        loading: false,
        items: action.payload.products
      };

    case FETCH_PRODUCTS_FAILURE:
      // The request failed. It's done. So set loading to "false".
      // Save the error, so we can display it somewhere.
      // Since it failed, we don't have items to display anymore, so set `items` empty.
      //
      // This is all up to you and your app though:
      // maybe you want to keep the items around!
      // Do whatever seems right for your use case.
      return {
        ...state,
        loading: false,
        error: action.payload.error,
        items: []
      };

    default:
      // ALWAYS have a default case in a reducer
      return state;
  }
}
複製程式碼

最後只需要把products傳遞給ProductList元件,這個元件最終顯示列表,同時也負責啟動資料獲取工作.

ProductList.js

import React from "react";
import { connect } from "react-redux";
import { fetchProducts } from "/productActions";

class ProductList extends React.Component {
  componentDidMount() {
    this.props.dispatch(fetchProducts());
  }

  render() {
    const { error, loading, products } = this.props;

    if (error) {
      return <div>Error! {error.message}</div>;
    }

    if (loading) {
      return <div>Loading...</div>;
    }

    return (
      <ul>
        {products.map(product =>
          <li key={product.id}>{product.name}</li>
        )}
      </ul>
    );
  }
}

const mapStateToProps = state => ({
  products: state.products.items,
  loading: state.products.loading,
  error: state.products.error
});

export default connect(mapStateToProps)(ProductList);
複製程式碼

這裡引用資料用了state.products.<somedata>,沒有用state.<somedata>, 因為我假設你可有有不止一個reducer,每個reducer處理自己的一塊state. 為了讓多個reducer一同工作,我們需要rootReducer.js檔案, 它會把所有的小塊reducer組合在一起:

rootReducer.js

import { combineReducers } from "redux";
import products from "./productReducer";

export default combineReducers({
  products
});
複製程式碼

截止,在建立store時, 可以傳遞這個"root" reducer:

index.js

import rootReducer from './rootReducer';

// ...

const store = createStore(rootReducer);
複製程式碼

Redux中的錯誤處理

這裡的錯誤處理內容很少,但是基礎的結構和執行api呼叫的action是一樣的. 總體的思路是:

  1. 在呼叫失敗時dispatch FAILURE
  2. 在reducer中通過設定標識或儲存出錯資訊來處理 FAILURE action.
  3. 向元件傳遞錯誤標識或者資訊,根據需要條件性渲染錯誤資訊

但是它將會渲染兩次

這是一個常見的擔憂.的確是會渲染超過不止一次. 在state為空的時候渲染一次, 根據loading state會重新渲染,在顯示資料時還要渲染. 恐怖! 三次渲染!(如果直接跳過loading 會減微微兩次).

你之所以擔心不必要的渲染是因為效能的考慮,但是不要擔心,單個渲染速度很快. 如果明顯很慢,那就需要找到導致變慢的原因.

這樣考慮:app需要在沒有內容時顯示一些東西,可以是載入提示,或者錯誤提示. 你也不願意在在資料到來之前顯示空白頁面.這些提示為了我們增強使用者體驗的機會.

但是元件不應該自己去獲取資料

從構架的觀點看,如果一個父"東東"(元件,函式,或路由)在他載入元件之前自動獲取資料就更好了. 元件自己覺察不到無意義的呼叫. 他們可以幸福的等待資料.

有一些方法可以修復這個問題,但是凡事都有得有失. 魔術載入是魔術,它們需要更多的程式碼.

解決資料獲取的不同方法

有很多方式可以重構這段程式碼.沒有最好的方法,因為每個方法都有適用範圍, 因為對一個例項是最好的對於其他的例項未必如此.

componentDidMount中獲取資料不是最好的一個,但是是最簡單的完成工作的方法.

如果你不喜歡這麼作,還要其他一些方法可以嘗試:

  • 把API呼叫從Redux Action中轉移到api模組中, 在action中呼叫(分離關注點).
  • 元件直接呼叫API模組,然後在資料返回時從內部元件dispatch action. 類似 Dan Abramov的演示
  • 使用類似redux-dataloader或者redu-async-loader.或者Mark Eriksons的資料獲取庫方法之一.
  • 使用一個包裝元件用於執行fetch操作- 在上面的例子中可以稱為ProductListPage.之後"Page"關注fetching,"List"僅僅接受資料並渲染他們
  • 使用recompose庫把componentDidMount周期函式放入到高階包裝函式內-這個庫是可以工作,但是可能作者要停止了[^譯註,這個庫的作者就是React的核心成員,recompose算是react hooks的前身]
  • 很快你就可以使用React內建的suspense特性來獲取資料了.

程式碼

CodeSandbox完整程式碼

可用的api

完成!

相關文章