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
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 提供了一種方法,解決了全局數據傳遞的問題,使得組件之間不用顯式地經過 props 傳遞數據。ide
Context
對象,該對象擁有 Provider 和 Consumer 屬性。使用 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 解決了數據跨層級傳輸的問題,可是還遺留了一些問題:
<Consumer>{ value => <></></Consumer>
不優雅。不要緊,且看 hooks 閃亮登場,將這些問題一一擊破。
考慮到有些朋友不是很瞭解 hooks,本文先介紹一下 hooks 的基本用法 。Hooks 讓咱們能夠在 function 組件中使用狀態和生命週期函數,並賦予了一些更強大的功能。這也意味着,在 React 16.8 以後,咱們再不須要寫 class 組件。再強調一次,咱們再不須要寫 class 組件!
useState
: 容許在 function 組件中,聲明和改變狀態。在此以前,只有 class 組件能夠。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
。
在 react 引入 hooks 後,使得 context 的消費更簡單了,開發者能夠很優雅地直接獲取。下面咱們使用 useContext
對 User
組件進行重構。
// 重構前 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
訪問它。
當組件同時使用多個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
會返回 state
和 dispath
方法,返回的 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 時代狀態管理的最佳實踐。