今天,我们将探讨如何开发自定义 Hook 来管理全局状态 —— 这是一种比 Redux 更易于使用、比 Context API 性能更好的方法。
Hook 基础知识
如果你已经熟悉 React Hook,可以跳过此部分。
useState()
在 Hook 之前,函数组件没有状态。现在,有了 useState()
,我们可以实现状态。
它通过返回一个数组来实现。数组的第一项是一个变量,用于访问状态值。第二项是一个函数,用于更新组件状态以反映 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。
我们可以通过在自定义 Hook 中调用 useState()
来实现。但是,我们不返回 setState()
函数,而是将其添加到侦听器数组中,并返回一个更新状态对象并运行所有侦听器函数的函数。
use-global-hook
我创建了一个 NPM 包,封装了所有这些逻辑。
你将不需要在每个项目中重新编写此自定义 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;
评论(0)