如何在常見業務場景中使用React Hook

    本文將經過如何使用React Hook的API來構建react項目,摒棄傳統的redux,經過使用useReducer和useContext等來實現狀態分發管理,在最後會講述如何在React Hook項目進行異步的數據請求。 Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。如何你對React Hook的概念還不是特別清楚,請移步React官網的相關介紹查看這個新特性。javascript

Provier組件

    瞭解redux的同窗應該知道react-redux中的Provier組件,經過Provider組件能夠實現將寫好的store進行狀態分發到下級任意一個子組件中。其實去查看Provier的實現源碼,能夠發現正好是使用了react的context屬性,因此咱們在這裏一樣使用React Hook的useContext屬性實現狀態分發。html

useContext

const value = useContext(myContext)
複製代碼

useContext接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider> 的 value prop 決定。java

當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值。react


除此以外,還得配合上useReducerios

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
複製代碼

useReducer是useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,並返回當前的 state 以及與其配套的 dispatch 方法。(若是你熟悉 Redux 的話,就已經知道它如何工做了。)git


有了這兩項API咱們就能夠開始編寫Provider組件了。首先建立context目錄,建立好基本的reducer和index文件,mainReducer.js實現代碼以下:github

import mainConstants from './mainConstants';

export const mainInitialState = {
    error: '',
    res: [],
    url: '/pool/query',
    loading: false,
};

export default (state, action) => {
    switch (action.type) {
        case mainConstants.INIT_PAGE:
            return {...state, res: action.payload};

        case mainConstants.TO_SEARCH:
            return {...state, url: action.payload};

        case mainConstants.PAGE_LOADING:
            return {...state, loading: action.payload};

        case mainConstants.CHANGE_ERROR:
            return {...state, error: action.payload};

        default:
            return state;
    }
};
複製代碼

index.js實現代碼以下:redux

import React, { useReducer, createContext } from 'react';
import mainReducer, {mainInitialState} from './main/mainReducer';

const context = createContext({});
const reducer = {
    main: mainReducer
};

// 添加狀態更改的log
function middlewareLog(lastState, action) {
    const type = action.type.split('_')[0].toLowerCase();
    const nextState = reducer[type](lastState, action);
    console.log(
        `%c|------- redux: ${action.type} -------|`,
        `background: rgb(70, 70, 70); color: rgb(240, 235, 200); width:100%;`,
    );
    console.log('|--last:', lastState);
    console.log('|--next:', nextState);
    return nextState;
}

const Provider = props => {
    const [mainState, mainDispatch] = useReducer(middlewareLog, mainInitialState);
    const combined = {
        main: {
            state: mainState,
            dispatch: mainDispatch,
        },
    };
    return (<context.Provider value={combined}> {props.children} </context.Provider>) }; export {context}; export default Provider; 複製代碼

而後在項目的主入口,加入Provier組件,實現狀態分發管理,axios

import React from 'react';
import ReactDOM from 'react-dom';
import Provider from './context/';
import App from './App';

ReactDOM.render(<Provider> <App /> </Provider>, document.getElementById('root'));
複製代碼

狀態分發已經建立完畢,接下來是看如何在組件中獲取狀態和使用狀態。App.js代碼以下:數組

import React, {useContext} from 'react';
import {context} from "./context";

function App() {
    const {state, dispatch} = useContext(context).main;
    return (
        <div> <div> hello </div> </div>
    );
}

export default App;
複製代碼

接下來咱們設定一個業務場景,一、頁面初始加載數據,二、能夠根據請求參數進行從新加載數據。 根據redux的三大原則,建立action文件,代碼以下:

import mainConstants from './mainConstants';

export const initPage = value => ({type: mainConstants.INIT_PAGE, payload: value});

export const toSearch = value => ({type: mainConstants.TO_SEARCH, payload: value});

export const pageLoading = value => ({type: mainConstants.PAGE_LOADING, payload: value});

export const changeError = value => ({type: mainConstants.CHANGE_ERROR, payload: value});
複製代碼

mainConstants代碼以下:

const create = str => 'MAIN_' + str;

export default {
    INIT_PAGE: create('INIT_PAGE'),
    TO_SEARCH: create('TO_SEARCH'),
    PAGE_LOADING: create('PAGE_LOADING'),
    CHANGE_ERROR: create('CHANGE_ERROR'),
}
複製代碼

再想一下業務場景,想要在頁面渲染的時候去獲取數據如何作呢?根據搜索框提供的參數又如何來向接口傳遞呢?這邊主要使用到了useEffect。

useEffect

useEffect(didUpdate);
複製代碼

useEffect可讓你在函數組件中執行反作用,操做數據獲取,設置訂閱以及手動更改 React 組件中的 DOM 都屬於反作用。

若是你熟悉 React class 的生命週期函數,你能夠把 useEffect Hook 看作 componentDidMount,componentDidUpdate 和 componentWillUnmount這三個函數的組合。

它接受兩個參數,第一個參數是一個執行函數,這個執行函數是怎麼處理以及何時執行,須要看第二個參數,通常地若是想執行只運行一次的 effect(僅在組件掛載和卸載時執行),能夠傳遞一個空數組([]),相似於生命週期中的componentDidMount。若是想要根據狀態值去執行,那麼只須要將狀態值傳入數組便可。這個API有不少規範須要探究,能夠移步官方文檔的FAQ進行查看。


接着改寫App.js, 代碼以下:

import React, {useContext, useEffect} from 'react';
import {context} from "./context";
import * as mainAction from "./context/main/mainAction";
import {getJson} from "./util";

function App() {
    const {state, dispatch} = useContext(context).main;

    // 設計內部變量ignore,而且在ignore爲True時改變狀態,
    // 最後返回一個執行操做,目的在於組件卸載時,禁止修改狀態
    useEffect(() => {
        let ignore = false;
        const getData = async () => {
            try {
                dispatch(mainAction.pageLoading(true));
                const res = await getJson(state.url);
                if (!ignore) {
                    dispatch(mainAction.initPage(res));
                }
                dispatch(mainAction.pageLoading(false));
            } catch (err) {
                if (!ignore) {
                    dispatch(mainAction.changeError(err.message));
                }
            }
        };
        getData();
        return () => {
            ignore = true
        };
    }, [state.url, dispatch]);  // 只在url更改的時候執行

    return (
        <div> <div> <button onClick={() => dispatch(mainAction.toSearch(state.url + '?bagName=224-truck2_2019-05-09-14-28-19_41-0'))}>search</button> {state.res.map(item => <p key={item.id}>{item.bagName}</p> )} </div> </div>
    );
}

export default App;
複製代碼

這邊是封裝了一個axios的請求函數,代碼以下:

import axios from 'axios';

const instance = getDefaultInstance();

export function getJson(url, data) {
    return instance.get(url, { params: data });
}

function getDefaultInstance() {
    const instance = axios.create({
        baseURL: '/',
        withCredentials: true
    });
    instance.interceptors.response.use(res => {
        return res.data.data;
    }, err => {
        throw err;
    });
    return instance;
}
複製代碼

官方文檔中說明,但願咱們將異步請求的函數直接放在useEffect中,而不是在組件內。

以上就實現了最初設定的業務場景,不過還有一個能夠優化的地方。像平時工做中,幾乎每一個頁面都會有初始請求數據和查詢數據的功能,因此咱們能夠自定義Hook,將相同邏輯的部分實現封裝。

自定義Hook

當咱們想在兩個函數之間共享邏輯時,咱們會把它提取到第三個函數中。而組件和 Hook 都是函數,因此也一樣適用這種方式。

自定義 Hook 是一個函數,其名稱以 「use」 開頭,並且官方規定,必需要以「use」開頭,函數內部能夠調用其餘的 Hook

想法是自定義的hook本身管理state,因此這裏用到了useState,


useState

const [state, setState] = useState(initialState);
複製代碼

useState是返回一個 state,以及更新 state 的函數。

在初始渲染期間,返回的狀態 (state) 與傳入的第一個參數 (initialState) 值相同。

setState 函數用於更新 state。它接收一個新的 state 值並將組件的一次從新渲染加入隊列。

在後續的從新渲染中,useState 返回的第一個值將始終是更新後最新的 state。


改寫App.js,代碼以下:

import React, {useContext, useEffect, useState} from 'react';
import {context} from "./context";
import * as mainAction from "./context/main/mainAction";
import {getJson} from "./util";

function useInitPage({state, dispatch, action}) {
    const [res, setRes] = useState(state.res);
    const [url, setUrl] = useState(state.url);

    const addValue = url => setUrl(url);

    useEffect(() => {
        let ignore = false;
        const getData = async () => {
            try {
                dispatch(action.pageLoading(true));
                const res = await getJson(url);
                if (!ignore) {
                    setRes(res); // 也能夠不返回res
                    dispatch(action.initPage(res));
                }
                dispatch(action.pageLoading(false));
            } catch (err) {
                if (!ignore) {
                    dispatch(action.changeError(err.message));
                }
            }
        };
        getData();
        return () => { ignore = true };
    }, [url, dispatch]);

    return {res, addValue};
}

function App() {
    const {state, dispatch} = useContext(context).main;
    const {res, addValue} = useInitPage({state, dispatch, action: mainAction});
    
    useEffect(() => {
        if (state.error !== '') {
            alert(state.error);
        }
    }, [state.error]);

    return (
        <div> <div> <button onClick={() => addValue(state.url + '?bagName=224-truck2_2019-05-09-14-28-19_41-0')}>search</button> {res.map(item => <p key={item.id}>{item.bagName}</p> )} </div> </div>
    );
}

export default App;
複製代碼

總結

    本文主要經過React Hooks的幾個新的API來實現了業務場景中的基礎部分,實現代碼略簡單,概念介紹的也簡單了些,不過從上手來看,仍是很是實用的。class組件存在某些問題使得上手難度略高,這也是官方推薦使用React Hook的緣由。官方最後公佈並無計劃移除class,不過仍是推薦class和Hook並行的策略。

附上github源碼地址

相關文章
相關標籤/搜索