如何去合理使用 React hook ?

react hook是react V16版本中最重要的新特性之一,不得不承认它的功能很强大,但是在项目开发中,发现有时候使用hook其实并不是那么的流…
关注者
640
被浏览
195,293

34 个回答

对后续感兴趣的可以关注专栏 zhuanlan.zhihu.com/fefa ,我会在那上面把该写的慢慢写了


我在公司内部有一个分享就是讲hook怎么用,公开其中一小部分内容。

首先我们看一个实际的hook实现:

const useUserList = () => {
    const [pending, setPending] = useState(false);
    const [users, setUsers] = useState([]);
    const load = async params => {
        setPending(true);
        setUsers([]);
        const users = await request('/users', params);
        setUsers(users);
        setPending(false);
    };
    const deleteUser = useCallback(
        user => setUsers(users => without(users, user)),
        []
    );
    const addUser = useCallback(
        user => setUsers(users => users.concat(user)),
        []
    );
    return [users, {pending, load, addUser, deleteUser}];
};

这就是一个标准的业务用hook,提供了一个用户列表,有加载、添加、删除三个功能。不如说对于大部分的一线业务系统,能从这个粒度上去玩hook已经是前10%的水准了吧。

但是,这个hook的实现其实是有问题的,这个hook包含了多方向的功能,让我们拆一拆:

  1. 加载一个远程数据,并且控制“加载中”状态。
  2. 往一个数组中增加或删除内容。
  3. 将一份数据(列表)和这份数据的相关操作(add、delete)合在一起返回。
  4. 指定加载用户列表这个具体业务场景。

一但这样去拆解,不难发现其实1-3全是通用能力,而不是业务相关的。

所以我得出来一个比较经典的hook的分层拆解的玩法。

状态与操作封装

如同面向对象强调的是状态(properties)与操作(methods)的封装,虽然我们在React里大量追求函数式,但也并不代表我们应该反对面向对象的封装特性。

把一个状态和它强相关的行为放在一起,显而易见地是一种合理的编程模式。

因此,在hook分层的最底层,我建议大家都有一个功能有,叫作“给我一个值和一堆方法,我帮你变成hook”,在我的实现里我叫它useMethods。这个东西超容易实现:

export const useMethods = (initialValue, methods) => {
    const [value, setValue] = useState(initialValue);
    const boundMethods = useMemo(
        () => Object.entries(methods).reduce(
            (methods, [name, fn]) => {
                const method = (...args) => {
                    setValue(value => fn(value, ...args));
                };
                methods[name] = method;
                return methods;
            },
            {}
        ),
        [methods]
    );
    return [value, boundMethods];
};

什么你说太绕了都快晕了?玩React哪有不绕的道理……

封装常用数据结构

有了与任何类型都无关的基础的方法封装,我们就可以在它的基础上衍生出最常见的数据结构了。

正如原生的数组有push、pop、slice等方法,原生的字符串有trim、padStart、repeat等方法,把这些东西包一包也能变成“数组hook”、“字符串hook”这样的基础hook。

这里需要注意的是,你不能把useArray的push直接引到数组的push上去,因为我们对状态的更新要求是immutable的,所以push要对应concat,pop要对应slice,总之这是很容易的:

const arrayMethods = {
    push(state, item) {
        return state.concat(item);
    },
    pop(state) {
        return state.slice(0, -1);
    },
    slice(state, start, end) {
        return state.slice(start, end);
    },
    empty() {
        return [];
    },
    set(state, newValue) {
        return newValue;
    },
    remove(state, item) {
        const index = state.indexOf(item);
        if (index < 0) {
            return state;
        }
        return [...state.slice(0, index), ...state.slice(index + 1)];
    }
};

const useArray = (initialValue = []) => {
    invariant(Array.isArray(initialValue), 'initialValue must be an array');
    return useMethods(initialValue, arrayMethods);
};

相应的,数字我们也可以玩一玩:

const numberMethods = {
    increment(value) {
        return value + 1;
    },
    decrement(value) {
        return value - 1;
    },
    set(current, newValue) {
        return newValue;
    }
};

const useNumber = (initialValue = 0) => {
    invariant(typeof initialValue === 'number', 'initialValue must be an number');
    return useMethods(initialValue, numberMethods);
};

随你高兴吧,有闲情的可以把什么链表、树、队列、栈、堆、冠军树、红黑树全给来一遍,你高兴就好。

通用过程封装

数据结构毕竟还只是最基础的东西,我们不能只有数据结构就去写代码,我们还需要利用数据结构串起来的过程。

比如在最前面的例子里,对“异步调用”这个事情就是一个很经典的过程。

因此,我们可以有这样的一个hook,它的作用是“给我一个异步函数,我帮你调用它并管理异步状态”,我叫它useTaskPending,功能也简单,直接用useNumber去管一管异步状态就好:

const useTaskPending = task => {
    const [pendingCount, {increment, decrement}] = useNumber(0);
    const taskWithPending = useCallback(
        async (...args) => {
            increment();
            const result = await task(...args);
            decrement();
            return result;
        },
        [task, increment, decrement]
    );
    return [taskWithPending, pendingCount > 0];
};

再给它进一步,我们想要不仅仅能调用过程,还能把结果给同步到状态里:

const useTaskPendingState = (task, storeResult) => {
    const [taskWithPending, pending] = useTaskPendingState(task);
    const callAndStore = useCallback(
        () => {
            const result = await taskWithPending();
            storeResult(result);
        },
        [taskWithPending, storeResult]
    );
    return [callAndStore, pending];
};

拼装成业务

有数据结构,有过程,现在再去拼一个业务就简单了,像这样:

const useUserList = () => {
    const [users, {push, remove, set}] = useArray([]);
    const [load, pending] = useTaskPendingState(listUsers, set);
    return [users, {pending, load, addUser: push, deleteUser: remove}];
};

你可以看到,很直观地是代码少了那么几行,进而每一行代码都有更强的语义化了,比如useArray明确这里就是一个数组,对比useState还要去看参数才知道是数组还是对象干净利落了不少。

更重要的是,基于前面的方法、数据结构、过程这3层,你可以更快地搞出“文章列表”、“评论列表”、“用户详情”等等一系列的业务,而不需要重复地去管理pending、管理数组之类的冗余的事情。


兴致有限,就先简单地介绍一下hook最最基础的状态管理部分的实践玩法。顺便这代码能不能跑我不知道,只代表想法不代表实现~

其它如context怎么玩、effect怎么玩、ref有多牛逼、memo有多坑、subscription怎么用,甚至怎么快速写一个小型redux等等,就不赘述了。如果你想知道,可以投简历啊

2017年有一次, @baixuan 搞了个方案,我跟 @流形@黄子毅 一起去围观,大致是这样的:

<merge>
  <of value={1} />
  <from value={[1, 2, 3]} />
  <map operator={(i) => i*3}>
    <interval value={1000}>
  </map>
</merge>


这是基于 RxJS 的操作符,搞的声明式代码,其实挺适合编排的,但是业务使用的代价过高了,现在回头看,也不算比 hooks 高啊。目前搞 RxJS 和 React 结合的方案,最好的应该是 @太狼 @邹润阳 他们搞的那个。

来开点脑洞,会有人把 hooks 再搞成这个形态玩吗?