将 hook api 用于组件间通讯
前言
虽然在 React
中父子组件通讯非常便利, 但是兄弟间, 或者隔了好几代的组件间通讯相对比较麻烦. 一般在这种情况下会选择引入 Redux
或是 Mobx
作为一个全局, 或者一个模块内的状态管理工具. 尽管他们的体积都比较庞大, 但是在这个需求面前他们依旧是必不可少的配置之一.
然而随着 Hook Api
的问世界, 其无限的可能性让人浮想连篇.
本文将会详述如何运用 Hook Api
替代 Redux
/ Mobx
来担当其组件间甚至是全局的状态管理工具.
本文将会首先会简短的介绍下 Hook Api
之后将会介绍如何使用他们来实现任意粒度的状态管理.
Hook Api
Hook Api
是 react 于 2018 年提出的最新Api, 目前(2018年11月18日)还处于 alpha 阶段(React 16.7.0-alpha.2). 该 Api 的主要功能在于强化了 functional component
对 state
以及生命周期函数(例如 componentDidMount
等) 的操作能力, 使得 functional component
不再是 stateless component
, 而成为了 stateful component
, 并且能够和class component
一样使用生命周期函数. 不仅如此, Hook Api
还大大增强了逻辑的可复用性, 在笔者看来,该 Api 必��被各大前端框架借鉴并引领一场 Api 的革命.
关于 Hook Api
更详细的介绍可参考 2018 年的 React Conf 或是 官方文档
useState
对于一个 React
中传统的 class component
来说, 更新 state
不仅需要在 constructor
中初始化, 还需要每次都调用 setState
来通知 UI 更新. 代码通常相当冗余. 因此传统的 React
设计模式通常都建议程序员将组件拆分成负责逻辑的 container
以及负责 UI 展现的 component
. container
负责各种 state
逻辑更新, 生命周期函数操作, 并将需要渲染的数据通过 props
传递给 负责 UI 展现的 component
. 而这个 component
不保有任何 state
(也因此称之为 stateless component
), 只是根据 container
传递的 props
渲染.
这种传统的做法往往使得很简单的事情需要相当大的代码量才能完成. 简单来说, 对于一个计数器 (为了简化代码, 并没有拆分成 container
和 component
)
class Counter extends React.Component {
constructor(props) {
this.state = {
counter: 0
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({
...this.state,
counter: this.state.counter + 1
});
}
render() {
return (
<div>
<p>{this.state.counter}</p>
<button onClick={this.handleClick}>
点我
</button>
</div>
);
}
}
可以看到, 仅仅一个counter, 就需要 20 多行的代码才能完成, 并且大部分的代码都用来处理诸如 setState
需要整个新的 state
而不是只更新其中的一两个字段, javascript
天生的 this
指代混乱导致的在构造函数中的bind
. 并且如果其他地方也需要有 counter
点击事件的话, 该段代码中的逻辑并不能直接用于另一个组件,只能使用丑陋的复制粘帖.
为了解决这个问题, useState
应运而生. 顾名思义, useState
就是使用 state
的意思. 同样的对于上述 counter
, 现在只需要
function Counter() {
[counter, setCounter] = React.useState(0);
return (
<div>
<p>{counter}</p>
<button onClick={setCounter.bind(null, counter+1)}>
点我
</button>
</div>
)
}
即可, 代码量瞬间缩短了一倍, 并且可读性也好了很多(相比与之前许多意义不明的bind
, setState
中的 ...
).
更令人称赞的是, 当其他地方也需要点击计数功能时, 这段逻辑完全可以抽离成为单独的一段函数, 并且在多个组建中调用.
function Counter() {
[counter, inc] = useCounter(0);
return (
<div>
<p>{Counter}</p>
<button onClick={inc}
点我
</button>
</div>
)
}
function useCounter(initialValue) {
[counter, setCounter] = React.useState(initialValue);
function inc() {
setCounter(counter + 1);
}
return [counter, inc];
}
更详细的介绍可以参考官方文档
useEffect
与 useState
相对, useEffect
则不那么的顾名思义. 其表示使用效果, 因为传统的 class component
中, componentDidMount
和 componentDidUpdate
都会带来各种效果, 也因为 React
团队想要淡化这两者的区别,因此 useEffect
就诞生了, 它被用于在functional component
中担任 componentDidMount
和 componentDidUpdate
的角色.
简单来说, 如果之前有
class Foo() {
constructor(props) {
super(props);
this.bar = this.bar.bind(this);
}
bar() {
console.log('123')
}
componentDidMount() {
window.addEventListener('resize', this.bar);
}
componentWillUnmount() {
window.removeEventListener('resize', this.bar)
}
}
现在则可以写成
function Foo() {
useEffect(() => {
window.addEventListener('resize', bar);
return () => {
window.removeEventListener('resize', bar);
}
}, []);
function bar() {
console.log('123')
}
}
useEffect
默认会在每次组件更新时都运行一次, 然而大部分情况下这并不是我们所想要的, 因此可以使用第二个参数来控制 useEffect
什么时候会再次被调用. useEffect
所接受的第二个参数为数组, 每当数组内的变量有更新时, 才会重新执行. 如果不赋值, 即以 useEffect(callback)
的方式调用, 则为默认模式, 每次更新都会调用useEffect
一次. 如果赋空数组,则表示它永远不被再次执行, 即原本的 componentDidMount
.
useEffect(callback, dep)
中的 callback
用于注销事件, 在每个useEffect
不再生效的时候, 该注销事件会被调用一次.
更详细的介绍可以参考官方文档
使用 Hook Api 做组件间的状态共享
useState
每次都会生成新的 state
和 setState
, 因此我们并不能直接通过 export
来让他们在组件间共享. ��是在发布-订阅者
模式的帮助下, 这依旧是可能的.
原理其实相当简单, 假设有三个组件A
, B
和 C
需要共享状态 . 那么我们可以在组件 A
B
C
中都调用一次 useState
, 各自更新各自的状态, 而每当这三个组件中有一个更新状态后, 立即发布消息通知剩余的两个, 使他们也同步的更新状态就可以了. 而同时新的状态也需要保存起来, 这样如果之后第四个组件也要一起共享该状态时, 只需要使用正确的初始值 useState(initialValue)
即可保证他们 4 个组件都保持同步.
完整代码
bindthis
装饰器需要自己实现, 网上有不少教程, npm上也有现成的库可以使用. 如果不喜欢装饰器的话, 也可以在 constructor
中挨个 bind
.
使用方法
简短来说, 对于 state
共享, 我们需要创建一个 Store
,来使得任意粒度的状态共享都可以实现(可以是全局, 也可以是模块内, 甚至只是两个组件间). 当然啦, 这个 fooStore
是需要 export
出去的, 而其他需要共享这个状态的组件只要 import
这个 fooStore
就行啦.
import Store from '@/utils/shared-state';
export const fooStore = new Store();
接���, 创建需要共享的状态
const initialValue = 0;
fooStore.createState('stateName', initialValue);
这样就配置好了一个工厂方法, 之后每当需要共享这个状态时, 只要在组件内
function Bar() {
const [fooState, setFooState] = fooStore.useState('stateName');
...
}
就能同步啦!
这样就可以跟 redux
还有 mobx
say good bye 了! 毕竟他们实在是不小啊...