You may see the title and wonder: why do this? react hooks is a useful invention that reorganizes the paradigm of writing and thinking about code, using smaller abstractions to split state and functions by function rather than centralizing them into a single state or lifecycle. But react hooks suffers from the notion of state dependencies, and worse: it relies on manual management - despite the official eslint rules provided by react, if you’ve used it, you’ll find it has a high rate of false positives - especially in complex components. So, does this mean that there is no solution? No, the author of vue3 spoke in one of his talks about the improvements of vue3 hooks over react hooks, one of which is that you don’t need to manually manage dependencies, watch dotJS 2019 - Evan You - State of Components. But react is really better in terms of ecology (including various libraries, IDE development experience), so I tried to implement automatic dependency management in react somehow, implementing some primitive hooks that behave similar to vue3.

The main sources of inspiration for this were solid.js and react-easy-state.

Thinking

List of common primitive hooks for vue3

  • ref
  • reactive
  • computed
  • watchEffect
  • watch

When it comes to variable state in react, mobx is probably the easiest one to think of (as opposed to the immutable redux), so the following is based on it to implement the above hooks.

Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { observer } from "mobx-react";
import { action, autorun, computed, observable, reaction } from "mobx";
import { useCallback, useEffect, useMemo, useState } from "react";

export function useVReactive<T extends object>(value: T): T {
  const [state] = useState(() => observable(value));
  return state;
}

export function useVRef<T>(value: T): { value: T } {
  return useVReactive({ value });
}

export function useVComputed<T>(fn: () => T): { value: T } {
  const computedValue = useMemo(() => computed(fn), []);
  return {
    get value() {
      return computedValue.get();
    },
  };
}

export function useVFn<T extends (...args: any[]) => any>(fn: T): T {
  return useCallback(action(fn), []);
}

export function useVWatch(deps: () => any, fn: () => void): void {
  useEffect(() => reaction(deps, fn), []);
}

export function useVWatchEffect(fn: () => void): void {
  useEffect(() => autorun(fn), []);
}

const contextMap = observable(new Map<ContextKey<any>, any>());
// eslint-disable-next-line
export interface ContextKey<T> extends Symbol {}
export function useVProvide<T>(key: ContextKey<T>, value: T): void {
  useState(action(() => contextMap.set(key, value)));
  useEffect(
    action(() => {
      contextMap.set(key, value);
      return action(() => {
        contextMap.delete(key);
      });
    }),
    []
  );
}
export function useVInject<T>(key: ContextKey<T>): T | undefined {
  const value = useMemo(() => computed(() => contextMap.get(key)), []);
  const state = useVRef(value.get());
  useVWatchEffect(() => (state.value = value.get()));
  return state.value;
}

export const defineComponent = observer;

Use

I wasn’t able to fully implement the vue3 hooks for a few reasons, for example

  • need to wrap functions that manipulate state with useVFn, whereas in vue3 it’s really just a matter of declaring normal functions in the setup function
  • useWatch uses a computed function, while vue3 uses an array of dependent states
  • You must use defineComponent to wrap components, in vue3 it’s just a hint for code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import {
  defineComponent,
  useVRef,
  useVFn,
  useVComputed,
} from "@liuli-util/mobx-vue3-hooks";

const HelloWorld = defineComponent(() => {
  const state = useVRef(0);
  const computedValue = useVComputed(() => state.value * 2);

  const onInc = useVFn(() => {
    state.value++;
  });
  return (
    <div>
      <button onClick={onInc}>增加</button>
      <div>{computedValue.value}</div>
    </div>
  );
});

Summary

The react ecology does have a variety of things, and the ecology is super rich, but certain details are really done relatively rough, especially with the official laissez-faire. I’ve often wondered why no one feels weird about the current stuff. For example, react-router v4=>v6 is not updating properly, material-ui/fluentui’s form and table components are almost unusable compared to antd, the complexity of redux is still necessary as the default state manager, react hooks dependency management relies on manual annoyance. css in js dozens of options why official don’t care and so on.