Hooks-Context状态管理的新选择

2次阅读

共计 6792 个字符,预计需要花费 17 分钟才能阅读完成。

React 16.3 版本,正式推了出官方推荐的 context API —— 一种跨层级的数据传递方法。React 16.8 版本,推出了全新的 hooks 功能,将原本只有 class 组件才有的状态管理功能和生命周期函数功能,赋予了 function 组件。Hooks 配合 context 一起使用,为 react 状态管理提供了一种新的选择。这可能会减少开发者对 redux 等状态管理库的依赖。

本文首先会对官方的 context 作简单介绍,并搭建一个十分简单的使用全局状态的应用。然后再对 hooks 的基本 API useState useEffect 做基本介绍。接着使用 useContext hooks 对应用进行重构,让 context 的使用变得更优雅。再使用 useReducer hooks 来管理多个状态。最后,待充分理解 hooks 和 context 之后,我们将它们搭配起来用,对整个应用进行状态管理。

Context 概述

React 中存在一个众所周知的难题,那就是如何管理全局状态。即便是最基础的全局状态跨越层级传递,也是非常麻烦。此时,首选的解决方案就是使用状态管理库,如 redux。Redux 本身是一个 API 非常少的状态管理工具,其底层也是用 context 实现的。在一些状态管理不是那么复杂,但是又有跨越层级传递数据的需求时,不妨考虑使用 context 直接实现。

例如,一个 Page 组件包含全局状态 user,需要经过多次 props 的传递,层级很深的 Avatar 组件才能使用它。

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

Context:跨层级传递数据

Context 提供了一种方法,解决了全局数据传递的问题,使得组件之间不用显式地通过 props 传递数据。

  • 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});
};
 // ... 渲染整颗组件树
}

有没有解决方案呢?当然有!

创建一个只接收 props.children的新组件 AppProvider,并将 App 组件中的逻辑都移到 AppProvider组件中。通过备注的 console 日志可以看到,该方式避免了不必要的渲染。

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 方案中:

// 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 时代状态管理的最佳实践。

正文完
 0