React Hooks 从入门到放弃(二)

avatar
@腾讯科技(深圳)有限公司

Hooks 的合理使用

作者最初使用React Hooks的场景就是用Hooks重构现有的 Class 组件。

改写 Class 组件也没有很难, 对照官网 API, 直接暴力输出

  1. 使用useState替代this.state
  2. 使用useCallback替代 Class 的方法
  3. 使用useEffect替代生命周期函数

首先, 来看一个使用 Class 实现的组件, 支持列表的增删查改(不好意思, 只实现了增);

Edit On CodeSandbox

重构组件

按照上面的暴力输出思路和官网提供的HooksAPI, 我们将一个 Class 组件改造成了 Function + Hooks 组件试试。

Edit on CodeSandbox

看上去好像没什么问题, 有一种重构代码大功告成的感觉。可内心总有一点点不安, 这不安来自于哪里呢?

在身边的同事看到这段代码后, 吐槽地说了一句, 使用React Hooks看上去好乱阿, 在一个函数里写了一坨代码, 不像OOP那样逻辑清晰

仔细想想, 好像确实是这个样子的。 重构后的组件代码逻辑都在同一个函数中, 看上去逻辑不清晰、可阅读性很差、维护困难。

这么简单的组件改写后都这么乱了, 逻辑更复杂一些的组件, 就是更大一坨的意大利面了==

代码优化

放弃使用 React Hooks? 难道它就真的不香吗?!

考虑到自己的代码能力是渣中本渣, 所以一定是自己的使用姿势有问题了。那就看看能不能继续优化下去吧!

1. 封装语义化 useEffect

在上面代码中, 使用useEffect模拟了componentDidMount这一生命周期。React 官方推荐使用多个useEffect去完成不同的事情, 而不是放在一起。那我们可以对此进行一定的封装处理。

// useMount sourcecode
import { useEffect } from 'react';
const noop = () => {};
export function useMount(mount, unmount = noop) {
    useEffect(() => {
        mount();
        return () => {
            unmount();
        };
    }, []);
}

// use case
export default function Demo(props) {
    useMount(() => {
        // do something
        // after didMount
    });
}

2. 封装 Hooks

在上述组件中, 使用了useState, 并在添加,编辑,删除等操作中都调用了修改 State 的setXXX方法。

像这样的数据-操作有着相关联系的, 我们可以封装自己的 Hooks

import { useState, useCallback } from 'react';

export function useList(initial = []) {
    const [list, setList] = useState(initial);

    const add = useCallback(data => {
        // setXXX使用函数后, 入参会拿到最新的state数据
        setList(list => [...list, data]);
    }, []);
    const edit = useCallback(data => {}, [list]);
    const deleteOne = useCallback(data => {}, [list]);

    return [list, add, edit, deleteOne];
}

上述数据结构-操作方法只是最简单的一种方案。更多的使用方法和抽象封装要具体情况具体分析来使用了。

所以在上述封装之后,

export default function Demo() {

    const [list, add, edit, deleteOne] = useList([]);
    const handleAdd = useCallback(() => {
        add({
            title: `local data ${list.length}`,
            description: 'description test'
        });
    }, []);
    const handleEdit = useCallback(() => {}, []);
    const handleDelete = useCallback(() => {}, []);

    usetMount(() => {});
    //...
    // 其他不变
}

方法论

方法论主要解决“怎么办”的问题。

下文只是个人对React Hooks的实践总结, 如果有更好的思路欢迎来拍砖。

这里提到的方法论, 是在使用函数组件 + React Hooks时, 我们该怎么合理的应用它。

首先, 将注意力回到最初提到的函数组件, 它实际上就是数据=>视图, 一组特殊的输入输出的函数。

v = f(p)

函数组件与React Hooks体现的都是函数式编程的思想, 即:

函数式编程是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ 演算(lambda calculus)为该语言最重要的基础。而且,λ 演算的函数可以接受函数当作输入(引数)和输出(传出值)。 比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

在上文中提到了数据 -> 视图 -> SideEffect这样的流程。

每一次渲染都是不同情况引起 -> 数据变更 -> 视图更新 -> 执行SideEffect的过程, 这是一条主线的渲染流程, 那可以进行如下重组:

每一次渲染 === 多个(数据, 操作指令, 副作用) => 视图 => SideEffect处理

我们更需要关心(数据, 操作指令, 副作用)这个元组, 如何将多个元组在这个渲染流程中合并成一条数据 -> 视图 -> SideEffect是React Fiber架构实现的事情。这个心智操作由React框架解决。我们只要正确实现(数据, 操作指令, 副作用)的封装就好了。

而这是我们可以使用React Hooks做到的事情, 也是我们如何合理的封装使用React Hooks的方法论。

个人觉得倒计时是一个不错的例子

import React, { useState, useCallback, useEffect, useRef } from "react";

export function useCount() {
  const [count, setCount] = useState(0);
  const timer = useRef(null);

  useEffect(() => {
    timer.current = setTimeout(() => {
      if (count > 0) {
        setCount(count - 1);
      }
    }, 1000);

    return () => {
      clearTimeout(timer.current);
    };
  }, [count]);

  const startCount = useCallback(count => {
    setCount(count);
  }, []);

  const stopCount = useCallback(() => {
    clearTimeout(timer.current);
  }, []);

  return [count, startCount, stopCount];
}

我们使用useCount这个封装的Hook, 提供了数据->count, 操作->startCount, 内部使用useEffect封装了使用setTimeout倒计时的逻辑。

Edit on CodeSandbox

总结

本文着重点在于如何合理地使用React Hooks, 提出了对书写函数组件+ReactHooks的方法论的思考。

封装Hooks也是基于可扩展可维护的实用角度出发。 本文也是提醒自己不要为了写Hooks而写Hooks。