将 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 componentstate 以及生命周期函数(例如 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 渲染.

这种传统的做法往往使得很简单的事情需要相当大的代码量才能完成. 简单来说, 对于一个计数器 (为了简化代码, 并没有拆分成 containercomponent)

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 中, componentDidMountcomponentDidUpdate 都会带来各种效果, 也因为 React 团队想要淡化这两者的区别,因此 useEffect 就诞生了, 它被用于在functional component 中担任 componentDidMountcomponentDidUpdate 的角色.

简单来说, 如果之前有

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 每次都会生成新的 statesetState, 因此我们并不能直接通过 export 来让他们在组件间共享. ��是在发布-订阅者模式的帮助下, 这依旧是可能的.

原理其实相当简单, 假设有三个组件A, BC 需要共享状态 . 那么我们可以在组件 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 了! 毕竟他们实在是不小啊...