首页
Preview

使用 React Hooks 进行状态管理 —— 不需要 Redux 或 Context API

今天,我们将探讨如何开发自定义 Hook 来管理全局状态 —— 这是一种比 Redux 更易于使用、比 Context API 性能更好的方法。

Hook 基础知识

如果你已经熟悉 React Hook,可以跳过此部分。

useState()

在 Hook 之前,函数组件没有状态。现在,有了 useState(),我们可以实现状态。

image.png

它通过返回一个数组来实现。数组的第一项是一个变量,用于访问状态值。第二项是一个函数,用于更新组件状态以反映 DOM 上的新值。

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() 函数允许你在函数组件中执行副作用。

默认情况下,效果会在每次完成渲染后运行。但是,你可以选择仅在某些值发生更改时触发它,通过将变量数组作为第二个可选参数传递。

// Without the second parameter
useEffect(() => {
  console.log('I will run after every render');
});

// With the second parameter
useEffect(() => {
  console.log('I will run only when valueA changes');
}, [valueA]);

要实现与 componentDidMount() 相同的结果,我们可以发送一个空数组。由于空集永远不会更改,因此效果仅运行一次。

// With empty array
useEffect(() => {
  console.log('I will run only once');
}, []);

共享状态

我们可以看到,Hooks 状态的工作方式与类组件状态完全相同。组件的每个实例都有自己的状态。

要解决在组件之间共享状态的问题,我们将创建一个自定义 Hook。

image.png

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

use-global-hook

我创建了一个 NPM 包,封装了所有这些逻辑。

use-global-hook

你将不需要在每个项目中重新编写此自定义 Hook。如果你只想跳过并使用最终解决方案,则可以通过运行轻松将其添加到你的项目中:

npm install 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(() => {
    // Called just after component mount
    listeners.push(newListener);
    return () => {
      // Called just before the component unmount
      listeners = listeners.filter(listener => listener !== newListener);
    };
  }, []);
  return [state, setState];
};

第二个版本

除了最后一个修改,我们还将:

  • 将 React 作为参数设置,不再导入它。
  • 不导出自定义 Hook,而是导出一个函数,该函数根据 initialState 参数返回一个新的自定义 Hook。
  • 创建一个 store 对象,其中包含 state 值和 setState() 函数。
  • setState()useCustom() 中的箭头函数替换为常规函数,以便我们可以将 store 绑定到 this
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(() => {
    // Called just after component mount
    this.listeners.push(newListener);
    return () => {
      // Called just before the component unmount
      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;

因为我们现在有一个更通用的 Hook,所以我们必须在存储文件中进行设置。

import React from 'react';
import useGlobalHook from './useGlobalHook';

const initialState = { counter: 0 };

const useGlobal = useGlobalHook(React, initialState);

export default useGlobal;

将操作与组件分离

如果你曾经使用过复杂的状态管理库,就会知道直接从组件中操纵全局状态不是最好的方法。

最好的方法是通过创建操作来分离业务逻辑,这些操作可以操作状态。因此,我希望我们的解决方案的最后一个版本不会给组件访问 setState() 函数,而是给出一组操作。

为了解决这个问题,我们的 useGlobalHook(React, initialState, actions) 函数将接收一个 action 对象作为第三个参数。关于这一点,有一些事情我想要添加:

  • 操作将访问 store 对象。因此,操作可以使用 store.state 读取状态,通过 store.setState() 写入状态,甚至使用 state.actions 调用其他操作。
  • 为了组织,操作对象可以包含其他操作的子对象。因此,你可以具有 actions.addToCounter(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;

译自:https://javascript.plainenglish.io/state-management-with-react-hooks-no-redux-or-context-api-8b3035ceecf8

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
一个人玩
先找到想要的,然后出发

评论(0)

添加评论