Hooks + Context:狀態管理的新選擇

React 16.3 版本,正式推了出官方推薦的 context API —— 一種跨層級的數據傳遞方法。React 16.8 版本,推出了全新的 hooks 功能,將本來只有 class 組件纔有的狀態管理功能和生命週期函數功能,賦予了 function 組件。Hooks 配合 context 一塊兒使用,爲 react 狀態管理提供了一種新的選擇。這可能會減小開發者對 redux 等狀態管理庫的依賴。react

本文首先會對官方的 context 做簡單介紹,並搭建一個十分簡單的使用全局狀態的應用。而後再對 hooks 的基本 API useState useEffect 作基本介紹。接着使用 useContext hooks 對應用進行重構,讓 context 的使用變得更優雅。再使用 useReducer hooks 來管理多個狀態。最後,待充分理解 hooks 和 context 以後,咱們將它們搭配起來用,對整個應用進行狀態管理。git

Context 概述

React 中存在一個衆所周知的難題,那就是如何管理全局狀態。即使是最基礎的全局狀態跨越層級傳遞,也是很是麻煩。此時,首選的解決方案就是使用狀態管理庫,如 redux。Redux 自己是一個 API 很是少的狀態管理工具,其底層也是用 context 實現的。在一些狀態管理不是那麼複雜,可是又有跨越層級傳遞數據的需求時,不妨考慮使用 context 直接實現。github

例如,一個 Page 組件包含全局狀態 user ,須要通過屢次props的傳遞,層級很深的 Avatar 組件才能使用它。redux

<Page user={user} />
// ... render ...
<PageLayout user={user}/>
// ... render ...
<NavigationBar user={user} />
// ... render ...
<Avatar user={user}  />

Context :跨層級傳遞數據

Context 提供了一種方法,解決了全局數據傳遞的問題,使得組件之間不用顯式地經過 props 傳遞數據。ide

  • React.createContext: 建立一個 Context 對象,該對象擁有 Provider 和 Consumer 屬性。
  • Context.Provider: 接受一個 value 參數,在 value 參數更新的時候通知 Consumer。
  • Context.Consumer: 訂閱 value 參數的改變。一旦 value 參數改變,就會觸發它的回調函數。

使用 context 重構的以後,跨層級傳遞數據就變得容易不少:函數

// 建立一個 context
const UserContext = React.createContext();

class App extends React.Component {
 state = { user: "崔然" };

 setUser = user => {
   this.setState({ user });
};

 render() {
   // 設置 context 當前值爲 {user, setUser}
   return (
     <UserContext.Provider value={{
         user:this.state.user,
         setUser: this.setUser 
    }}>
       <Page />
     </UserContext.Provider>
  );
}
}

// ... Page render ...
<PageLayout />
// ... PageLayout render ...
<NavigationBar />
// ... NavigationBar render ...
// 不管組件有多深,均可以**直接**讀取 user 值
<UserContext.Consumer>
{ ({user, setUser}) => <Avatar user={user} setUser={setUser}/> }
</UserContext.Consumer>

避免全局渲染

可是,在使用 context 時,有些寫代碼的小技巧,須要特別注意。否則在全局狀態改變時, Provider 的全部後代組件都會從新渲染。例如,用戶點擊 Avatar 組件後,將 崔然 更新爲 CuiRan,這時會調用根組件的 setUser 方法。根組件 setState({ user }) 更新狀態,會致使整顆組件樹從新渲染。工具

const Avatar = ({ user, setUser }) => {
 // 用戶點擊改變全局狀態
 return <div onClick={() => setUser("CuiRan")}>{user}</div>;
};

class App extends React.Component {
 state = { user: "崔然" };
 setUser = user => {
   this.setState({ user });
};
 // ... 渲染整顆組件樹
}

有沒有解決方案呢?固然有!ui

建立一個只接收 props.children的新組件 AppProvider ,並將 App 組件中的邏輯都移到 AppProvider組件中。經過備註的 console 日誌能夠看到,該方式避免了沒必要要的渲染。this

const Avatar = ({ user, setUser }) => {
 // 用戶點擊改變全局狀態
 return <div onClick={() => setUser("CuiRan")}>{user}</div>;
};

// 將 App 邏輯移到 AppProvider
const UserContext = React.createContext();
class AppProvider extends React.Component {
 state = { user: "崔然" };

 setUser = user => {
   this.setState({ user });
};

 render() {
   return (
     <UserContext.Provider
       value={{
         user: this.state.user,
         setUser: this.setUser
      }}
     >
      {this.props.children}
     </UserContext.Provider>
  );
}
}

// APP 只保留根組件最基本的 JSX 嵌套
const App = () => (
 <AppProvider>
   <Page />
 </AppProvider>
);

// ... Page not render ...
<PageLayout />
// ... PageLayout not render ...
<NavigationBar />
// ... NavigationBar not render ...
// Consumer 監聽到 Provider value 的改變
<UserContext.Consumer>
{/* **only** Avatar render */ }
{({user, setUser}) => <Avatar user={user} setUser={setUser}/>}
</UserContext.Consumer>

爲何?爲何把 App 上的全局狀態及設置狀態的方法移到 AppProvider 上,就能避免沒必要要的渲染?在 props.children 方案中:spa

// 1. App 自己沒有全局狀態改變,所以 <Page/> 不會重渲染
const App = () => (
 <AppProvider>
   <Page />
 </AppProvider>
);

// 2. Provider value 變化,所以會觸發 Consumer 的監聽函數。
<UserContext.Provider
 value={{
   user: this.state.user,
     setUser: this.setUser
}}
 >
{  /* 3. this.props.children 只是 <Page /> 的引用
  但並不會調用 <Page />,即調用 createElement('Page') */ }
{this.props.children}
</UserContext.Provider>

雖然,context 解決了數據跨層級傳輸的問題,可是還遺留了一些問題:

  1. Consumer 的回調取值的寫法 <Consumer>{ value => <></></Consumer> 不優雅。
  2. 單個狀態和狀態改變很好傳遞,可是多個狀態和對應的狀態改變傳遞依舊不方便。
  3. 多個全局狀態,如何管理?

不要緊,且看 hooks 閃亮登場,將這些問題一一擊破。

Hooks 概述

考慮到有些朋友不是很瞭解 hooks,本文先介紹一下 hooks 的基本用法 。Hooks 讓咱們能夠在 function 組件中使用狀態和生命週期函數,並賦予了一些更強大的功能。這也意味着,在 React 16.8 以後,咱們再不須要寫 class 組件。再強調一次,咱們再不須要寫 class 組件!

  1. useState: 容許在 function 組件中,聲明和改變狀態。在此以前,只有 class 組件能夠。
  2. useEffect:容許在 function 組件中,抽象地使用 React 的生命週期函數。開發者可使用更函數式的、更清晰的 hooks 的方式。

使用 hooks 對帶有本地狀態的 Avatar 組件進行重構說明:

import React, { useState, useEffect } from 'react';

const Avatar = ({ user, setUser }) => {
 // 建立 user 狀態和修改狀態的函數
 const [user, setUser] = useState("崔然");
 
 // 默認 componentDidMount/componentDidUpdate 時會觸發回調
 // 也可使用第二個參數,指定觸發時機
 useEffect(() => {
   document.title = `當前用戶:${user}`;
});
 
 // 使用 setUser 改變狀態
 return <div onClick={() => setUser("CuiRan")}>{user}</div>;
};

接着,咱們繼續瞭解 context 的 hooks 用法 —— userContext

useContext:更優雅的 context

在 react 引入 hooks 後,使得 context 的消費更簡單了,開發者能夠很優雅地直接獲取。下面咱們使用 useContextUser 組件進行重構。

// 重構前
const User = () => {
 return (
   <UserContext.Consumer>
    {({ user, setUser }) => <Avatar user={user} setUser={setUser} />}
   </UserContext.Consumer>
);
};

// 重構後
const User = () => {
 // 直接獲取,不用回調
 const { user, setUser } = useContext(UserContext);
 return <Avatar user={user} setUser={setUser} />;
};

就是這麼簡單!不管 context 包含什麼,是數字、字符串,仍是對象、函數,均可以經過useContext訪問它。

useReducer:自帶的狀態管理

當組件同時使用多個useState方法時,須要一個一個的聲明。狀態多了,就一大溜的聲明。好比:

const Avatar = ({ user, setUser }) => {
 const [user, setUser] = useState("崔然");
 const [age, setAge] = useState("18");
 const [gender, setGender] = useState("女");
 const [city, setCity] = useState("北京");
 // more ...
};

useReducer 實際是 useState 的一個變種,解決了上述多個狀態,須要屢次使用 useState 的問題。

當你看到 useReducer 時,是否是很是熟悉?想起了redux 中的 reducer 函數。對!React 提供的 useReducer 函數,它就是使用 (use) reducer 函數做爲參數。useReducer 接受的 reducer 參數,本質和 redux 的是同樣的。而後 useReducer 會返回 statedispath 方法,返回的 dispath ,本質上和 redux 的也是同樣的。

讓咱們使用 useReducer 將帶有本地狀態的 Avatar 組件重構一下:

const reducer = (state, action) => {
 switch (action.type) {
   case "CHANGE_USER":
     return { ...state, user: action.user };
   case "CHANGE_AGE":
     return { ...state, age: action.age };
   // more ...  
   default:
     return state;
}};

const Avatar = ({ user, setUser }) => {
 const [state, dispatch] = useReducer(
   reducer,
  { user: "崔然", age: 18 }
);

 return (
   <>
     <div onClick={() => dispatch({ type: "CHANGE_USER", user: "CuiRan" })}>
      {state.user}
     </div>
     <div onClick={() => dispatch({ type: "CHANGE_AGE", age: 17 })}>
      {state.age}
     </div>
   </>
)};

更進一步地,將 useReducer和直接對比 redux 試試,你會發現它們之間驚人的類似:

// react hooks
const [state, dispatch] = useReducer(reducer, [initialArg]);

// redux
const store = createStore(reducer, [initialArg])
const state = store.getState()
const dispatch = store.dispatch

還記得咱們再 context 中介紹的 provider 和 consumer 嗎?再聯想一下,它們的做用不就是和 react-redux 中的 provider 和 connect 如出一轍 —— 將數據跨層級的進行傳遞!

// react hooks
<GolbleContext.Provider value={{state,dispacth}} >
 <App/>
</GolbleContext.Provider>
// ... 跨層級傳遞 ...
const { state, dispacth } = useContext(GolbleContext);

// react-redux
<Provider store={store}>
 <App />
</Provider>
// ... 跨層級傳遞 ...
connect(mapStateToProps, actionCreators)(ConsumerComponent)

到如今爲止,react 可謂是自帶了大半個 redux 的 API 了。那麼咱們不就能夠把 redux 的狀態管理思路直接搬過來便可。

最後,只須要將全局狀態放到在 App 組件的頂層。最終的示例:

const Avatar = ({{ state, dispatch }) => {// ...})
// 使用全局狀態和 dispatch
const User = () => {
 const { state, dispatch } = useContext(UserContext);
 return <Avatar state={state} dispatch={dispatch} />;
};

// 生成全局狀態和 dispatch
const reducer = (state, action) => {// ...};
const AppProvider = ({ children }) => {
 const [state, dispatch] = useReducer(reducer, { user: "崔然", age: 18 });

 return (
   <UserContext.Provider
     value={{
       state: state,
       dispatch: dispatch
    }}
   >
    {children}
   </UserContext.Provider>
)};

完整示例見:https://github.com/jiangleo/h...

結論

在 hooks 和 context 出現以前,react 缺少自帶的全局狀態管理能力。即使很小的應用,一旦要用到全局狀態,要麼使用 props 多層級的進行傳輸,要麼就只能引入 redux 等第三方狀態管理工具。

在 hooks 和 context 出現以後,react 自身提供了一種簡單的全局狀態管理的能力。若是你的項目比較簡單,只有少部分狀態須要提高到全局,大部分組件依舊經過本地狀態來進行管理。這時,使用 hooks + context 進行狀態管理的是強烈推薦的。打蒼蠅,用不着大炮。

此外,咱們也觀察到,社區中一些新型的基於 hooks + context 的狀態管理庫正在快速崛起,好比 easy-peasy、constate。另外一方面,成熟的 redux 也在 7.x 版本,開始引入 hooks API 開始升級。咱們也會持續保持關注,探索 hooks 時代狀態管理的最佳實踐。

相關文章
相關標籤/搜索