使用React Hooks管理状态-无需Redux和Context API

大家好! 我的名字叫亚瑟(Arthur),我作为移动Web团队在VKontakte工作,我参与了VKUI项目-一个React-components库,并以此为基础编写了移动应用程序中的一些接口。 与全球国家合作的问题仍然对我们开放。 有几种众所周知的方法:Redux,MobX,Context API。 我最近遇到了AndréGardi 状态管理部门使用React Hooks的文章-No Redux或Context API ,作者在其中建议使用React Hooks来控制应用程序的状态。

钩子正在迅速进入开发人员的生活,提供了解决或重新考虑不同任务和方法的新方法。 它们不仅改变了我们对如何描述组件的理解,也改变了我们对如何使用数据的理解。 阅读文章的翻译和猫咪下翻译的评论。

图片

React挂钩比您想象的更强大


今天,我们将研究React Hooks,并开发一个用于管理应用程序全局状态的自定义钩子,该钩子比Redux实现更简单,并且比Context API更高效。

React Hooks基础知识


如果您已经熟悉钩子,则可以跳过此部分。

useState()


在出现钩子之前,功能组件无法设置局部状态。 随着useState()的出现,情况已经发生了变化。



该调用返回一个数组。 它的第一个元素是提供对状态值的访问的变量。 第二个元素是一个更新状态并重绘组件以反映更改的函数。

 import React, { useState } from 'react'; function Example() { const [state, setState] = useState({counter:0}); const add1ToCounter = () => { const newCounterValue = state.counter + 1; setState({ counter: newCounterValue}); } return ( <div> <p>You clicked {state.counter} times</p> <button onClick={add1ToCounter}> Click me </button> </div> ); } 

useEffect()


类组件使用生命周期方法(例如componentDidMount()来响应副作用。 useEffect()挂钩允许您在功能组件中执行相同的操作。

默认情况下,每次重画后都会触发效果。 但是您可以确保仅在更改特定变量的值并将它们以数组形式传递给第二个可选参数后才执行它们。

 //     useEffect(() => { console.log('     '); }); //    useEffect(() => { console.log('     valueA'); }, [valueA]); 

为了获得类似于componentDidMount()的结果,我们将一个空数组传递给第二个参数。 由于空数组的内容始终保持不变,因此效果只会执行一次。

 //     useEffect(() => { console.log('    '); }, []); 

国家分享


我们看到了钩子状态就像类组件状态一样工作。 每个组件实例都有其自己的内部状态。

为了在组件之间共享状态,我们将创建自己的钩子。



这个想法是创建一个侦听器数组,并且只有一个状态。 每次组件更改状态时,所有订阅的组件都会调用其getState()并因此而更新。

我们可以通过在自定义钩子中调用useState()来实现此useState() 。 但是我们没有返回setState()函数,而是将其添加到侦听器数组中,并返回了一个在内部更新状态对象并调用所有侦听器的函数。

等一下 它如何使我的生活更轻松?


是的,你是对的。 我创建了一个NPM包 ,其中封装了所有描述的逻辑。

您不必在每个项目中都实施它。 如果您不再想花时间阅读并希望查看最终结果,只需将此包添加到您的应用程序即可。

 npm install -s use-global-hook 

要了解如何使用软件包,请研究文档中的示例。 现在,我建议重点关注包装在内部的布置方式。

第一版


 import { useState, useEffect } from 'react'; let listeners = []; let state = { counter: 0 }; const setState = (newState) => { state = { ...state, ...newState }; listeners.forEach((listener) => { listener(state); }); }; const useCustom = () => { const newListener = useState()[1]; useEffect(() => { listeners.push(newListener); }, []); return [state, setState]; }; export default useCustom; 

在组件中使用


 import React from 'react'; import useCustom from './customHook'; const Counter = () => { const [globalState, setGlobalState] = useCustom(); const add1Global = () => { const newCounterValue = globalState.counter + 1; setGlobalState({ counter: newCounterValue }); }; return ( <div> <p> counter: {globalState.counter} </p> <button type="button" onClick={add1Global}> +1 to global </button> </div> ); }; export default Counter; 

此版本已提供共享状态。 您可以向应用程序添加任意数量的计数器,它们都将具有共同的全局状态。

但是我们可以做得更好


您要什么?

  • 卸载组件时,从数组中删除侦听器;
  • 使钩子更加抽象,以便在其他项目中使用;
  • 使用参数管理initialState
  • 用更实用的样式重写钩子。

在卸载组件之前调用函数


我们已经发现,使用空数组调用useEffect(function, [])componentDidMount()方式相同。 但是,如果在第一个参数中传递的函数返回了另一个函数,则将在卸载组件之前立即调用第二个函数。 就像componentWillUnmount()

因此,在第二个函数的代码中,您可以编写用于从侦听器数组中删除组件的逻辑。

 const useCustom = () => { const newListener = useState()[1]; useEffect(() => { //     listeners.push(newListener); return () => { //     listeners = listeners.filter(listener => listener !== newListener); }; }, []); return [state, setState]; }; 

第二版


除了此更新之外,我们还计划:

  • 传递React参数并取消导入;
  • 导出的不是customHook,而是返回具有给定initalState
  • 创建一个store对象,其中将包含state值和setState()函数;
  • 将箭头功能替换为setState()useCustom()的常用功能,以便可以将storethis关联。

 function setState(newState) { this.state = { ...this.state, ...newState }; this.listeners.forEach((listener) => { listener(this.state); }); } function useCustom(React) { const newListener = React.useState()[1]; React.useEffect(() => { //     this.listeners.push(newListener); return () => { //     this.listeners = this.listeners.filter(listener => listener !== newListener); }; }, []); return [this.state, this.setState]; } const useGlobalHook = (React, initialState) => { const store = { state: initialState, listeners: [] }; store.setState = setState.bind(store); return useCustom.bind(store, React); }; export default useGlobalHook; 

动作与组件分开


如果您曾经使用过复杂的状态管理库,那么您就会知道从组件操作全局状态并不是一个好主意。

通过创建用于更改状态的操作来分离业务逻辑会更正确。 因此,我希望该软件包的最新版本不提供对setState()访问,而是对一组操作的访问。

为此,我们向我们的useGlobalHook(React, initialState, actions)第三个参数。 只想添加一些评论。

  • 动作将有权访问store 。 这样,动作可以读取store.state的内容, store.setState()调用store.setState()更新store.setState()甚至可以通过store.actions调用其他store.actions
  • 为避免混乱,操作对象可以包含子对象。 因此,您可以将具有所有计数器动作的actions.counter.add(amount)转移到子对象: actions.counter.add(amount)

最终版本


以下代码段是NPM软件包use-global-hook的当前版本。

 function setState(newState) { this.state = { ...this.state, ...newState }; this.listeners.forEach((listener) => { listener(this.state); }); } function useCustom(React) { const newListener = React.useState()[1]; React.useEffect(() => { this.listeners.push(newListener); return () => { this.listeners = this.listeners.filter(listener => listener !== newListener); }; }, []); return [this.state, this.actions]; } function associateActions(store, actions) { const associatedActions = {}; Object.keys(actions).forEach((key) => { if (typeof actions[key] === 'function') { associatedActions[key] = actions[key].bind(null, store); } if (typeof actions[key] === 'object') { associatedActions[key] = associateActions(store, actions[key]); } }); return associatedActions; } const useGlobalHook = (React, initialState, actions) => { const store = { state: initialState, listeners: [] }; store.setState = setState.bind(store); store.actions = associateActions(store, actions); return useCustom.bind(store, React); }; export default useGlobalHook; 

使用范例


您不再需要处理useGlobalHook.js 。 现在,您可以专注于您的应用程序。 以下是使用该软件包的两个示例。

多个计数器,一个价值


根据需要添加任意数量的计数器:它们都将具有全局值。 每当计数器之一增加全局状态时,所有其他计数器将被重绘。 在这种情况下,父组件不需要重绘。
生活的例子

异步ajax请求


通过用户名搜索GitHub存储库。 我们使用async / await异步处理ajax请求。 我们用每个新搜索更新查询计数器。
生活的例子

好了


现在,我们在React Hooks上拥有自己的状态管理库。

译者评论


大多数现有解决方案本质上是单独的库。 从这个意义上讲,作者描述的方法很有趣,因为它仅使用内置的React功能。 此外,与现成的同一个Context API相比,此方法减少了不必要的重画次数,因此赢得了性能。

Source: https://habr.com/ru/post/zh-CN454348/


All Articles