React Hooks實現非同步請求例項—useReducer、useContext和useEffect代替Redux方案

Marckon發表於2019-02-16

本文是學習了2018年新鮮出爐的React Hooks提案之後,針對非同步請求資料寫的一個案例。注意,本文假設了:
1.你已經初步瞭解hooks的含義了,如果不瞭解還請移步官方文件。(其實有過翻譯的想法,不過印記中文一直在翻譯,就是比較慢啦)
2.你使用Redux實現過非同步Action(非必需,只是本文不涉及該部分知識而直接使用)
3.你聽說過axios或者fetch(如果沒有,那麼想象一下原生js的promise實現非同步請求,或者去學習下這倆庫)
全部程式碼參見倉庫: github | Marckon選擇hooks-onlineShop分支以及master分支檢視

本文並非最佳實踐,如有更好的方法或發現文中紕漏,歡迎指正!

前序方案(不想看可以直接跳過)

  • 不考慮引入Redux

通過學習React生命週期,我們知道適合進行非同步請求的地方是componentDidMount鉤子函式內。因此,當你不需要考慮狀態管理時,以往的方法很簡單:

class App extends React.Component{
    componentDidMount(){
        axios.get(`/your/api`)
            .then(res=>/*...*/)
    }
}
  • 引入Redux進行狀態管理

當你決定使用Redux進行狀態管理時,比如將非同步獲取到的資料儲存在store中,事情就開始複雜起來了。根據Redux的官方文件案例來看,為了實現非同步action,你還得需要一個類似於redux-thunk的第三方庫來解析你的非同步action

requestAction.js: 定義非同步請求action的地方

//這是一個非同步action,分發了兩個同步action,redux-thunk能夠理解它
const fetchGoodsList = url => dispatch => {
    dispatch(requestGoodsList());
    axios.get(url)
        .then(res=>{
            dispatch(receiveGoodsList(res.data))
        })
};

requestReducer.js: 處理同步action

const requestReducer=(state=initialState,action)=>{
    switch (action.type) {
        case REQUEST_GOODSLIST:
            return Object.assign({},state,{
                isFetching: true
            });
        case RECEIVE_GOODSLIST:
            return Object.assign({},state,{
                isFetching:false,
                goodsList:action.goodsList
            });
        default:
            return state;
    }
};

App Component :你引入redux store和redux-thunk中介軟體的地方

import {Provider} from `react-redux`;
import thunkMiddleWare from `redux-thunk`;
import {createStore,applyMiddleware} from `redux`;
//other imports

let store=createStore(
    rootReducer,
    //這裡要使用中介軟體,才能夠完成非同步請求
    applyMiddleware(
        thunkMiddleWare,
        myMiddleWare,

    )
);
class App extends React.Component{
    render(){
        return (
            <Provider store={store}>
                <RootComponent/>
            </Provider>
        )
    }
}

GoodsList Component :需要進行非同步請求的元件

class GoodsList extends React.Component{
    //...
    componentDidMount(){
        this.props.fetchGoodsList(`your/url`);
    }
    //...
}
const mapDispatchToProps={
    fetchGoodsList
}
export default connect(
    mapStateToProps,
    mapDispatchToProps
)(GoodsList);

完整程式碼:branch:master-onlineShop

使用HooksuseReducer()useContext()

總之使用Redux很累,當然,你可以不使用Redux,直接通過props層層傳遞,或者使用context都可以。只不過本文我們學過了useReducer,使用到了Redux的思想,總要試著用一下。

這裡你不需要引入別的任何第三方庫了,簡簡單單地使用React@16.7.0-alpha.2版本就好啦

很重要的一點就是——函式式元件,現在React推薦我們這麼做,可以基本上代替class寫法。

函式簽名

  1. useReducer(reducer,initialState)
  2. useContext(ctxObj)
  3. useEffect(effectFunction,[dependencyValues])

概覽-你需要編寫什麼

  1. action.js:

    • 我們還使用redux的思想,編寫action
  2. reducer.js:

    • 處理action,不同於reduxreducer,這裡我們可以不用提供初始狀態
  3. 根元件:

    • Provider提供給子元件context
    • useReducer定義的位置,引入一個reducer並且提供初始狀態initialState
  4. 子元件:

    • useContext定義的位置,獲取祖先元件提供的context
    • useEffect用於進行非同步請求

實現

1.action.js:我們使用action建立函式

const REQUEST_GOODSLIST = "REQUEST_GOODSLIST";
const RECEIVE_GOODSLIST = "RECEIVE_GOODSLIST";

//開始請求
const requestGoodsList = () => ({
    type: REQUEST_GOODSLIST
});

//接收到資料
const receiveGoodsList = json => ({
    type: RECEIVE_GOODSLIST,
    goodsList: json.goodsList,
    receivedAt: Date.now()
});

export {
    RECEIVE_GOODSLIST,
    REQUEST_GOODSLIST,
    receiveGoodsList,
    requestGoodsList,
}

2.reducer.js:判斷action的型別並進行相應處理,更新state

import {
    RECEIVE_GOODSLIST,
    REQUEST_GOODSLIST,
} from "../..";


export const fetchReducer=(state,action)=>{
    switch (action.type) {
        case REQUEST_GOODSLIST:
            return Object.assign({},state,{
                isFetching: true
            });
        case RECEIVE_GOODSLIST:
            return Object.assign({},state,{
                isFetching:false,
                goodsList:state.goodsList.concat(action.goodsList)
            });
        default:
            return state;
    }
};

3.根元件:引入reducer.js

import React,{useReducer} from `react`;
import {fetchReducer} from `..`;

//建立並export上下文
export const FetchesContext = React.createContext(null);

function RootComponent() {
    //第二個引數為state的初始狀態
    const [fetchesState, fetchDispatch] = useReducer(fetchReducer, {
            isFetching: false,
            goodsList: []
        });
    return (
        //將dispatch方法和狀態都作為context傳遞給子元件
         <FetchesContext.Provider value={{fetchesState,dispatch:fetchDispatch}}>
             //...
             //用到context的一個子元件
             <ComponentToUseContext/>
         </FetchesContext.Provider>
    )
}

4.子元件:引入FetchesContext

import {FetchesContext} from "../RootComponent";
import React, {useContext, useEffect,useState} from `react`;
import axios from `axios`;

function GoodsList() {

    //獲取上下文
    const ctx = useContext(FetchesContext);
    
    //一個判斷是否重新獲取的state變數
    const [reFetch,setReFetch]=useState(false);

    //具有非同步呼叫副作用的useEffect
    useEffect(() => {
        //首先分發一個開始非同步獲取資料的action
        ctx.dispatch(requestGoodsList());
            axios.get(proxyGoodsListAPI())
                .then(res=>{
                    //獲取到資料後分發一個action,通知reducer更新狀態
                    ctx.dispatch(receiveGoodsList(res.data))
                })
      //第二個引數reFetch指的是隻有當reFetch變數值改變才重新渲染
    },[reFetch]);

    return (
        <div onScroll={handleScroll}>
            {
                //children
            }
        </div>
    )
}

完整程式碼參見:branch:hooks-onlineShop

目錄結構

我的目錄結構大概這樣:

src
  |- actions
     |- fetchAction.js
  |- components
     |-...
  |- reducers
     |- fetchReducer.js
  |- index.js

注意點

  1. 使用useContext()時候我們不需要使用Consumer了。但不要忘記exportimport上下文物件
  2. useEffect()可以看做是class寫法的componentDidMountcomponentDidUpdate以及componentWillUnMount三個鉤子函式的組合。

    • 當返回了一個函式的時候,這個函式就在compnentWillUnMount生命週期呼叫
    • 預設地,傳給useEffect的第一個引數會在每次(包含第一次)資料更新時重新呼叫
    • 當給useEffect()傳入了第二個引數(陣列型別)的時候,effect函式會在第一次渲染時呼叫,其餘僅當陣列中的任一元素髮生改變時才會呼叫。這相當於我們控制了元件的update生命週期
    • useEffect()第二個陣列為空則意味著僅在componentDidMount週期執行一次
  3. 程式碼倉庫裡使用了Mock.js攔截api請求以及ant-design第三UI方庫。目前程式碼比較簡陋。

相關文章