DEV Community

Ryan John
Ryan John

Posted on • Originally published at vureact.top

How does VuReact generate React Hooks dependency arrays from your Vue 3 reactive state?

VuReact is a compiler toolchain for migrating from Vue to React — and for writing React with Vue 3 syntax. In this article, we dive straight into the core: how VuReact automatically analyzes reactive dependencies in Vue 3 and precisely generates React Hooks dependency arrays.

Before We Start

To keep the examples easy to read, this article follows two simple conventions:

  1. All Vue and React snippets focus on core logic only, with full component wrappers and unrelated configuration omitted.
  2. The discussion assumes you are already familiar with the reactive and dependency tracking mechanisms of both Vue and React.

Compilation Mapping

Vue automatic dependency analysis → React Hook dependency array generation

VuReact's compiler has built-in automatic dependency analysis. Following React's rules, it intelligently analyzes reactive accesses within top-level arrow functions and top-level variable declarations, generating accurate dependency arrays.

  • Vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';

  const count = ref(0);
  const foo = ref(0);
  const state = reactive({ foo: 'bar', bar: { c: 1 } });

  const fn1 = () => {
    count.value += state.bar.c;
    console.log(count.value);
  };

  const fn = () => {};

  const fn2 = () => {
    const c = foo.value;
    fn();

    const fn4 = () => {
      state.bar.c--;
      c + count.value;
    };
  };

  const fn3 = () => {
    foo.value++;

    const state = ref('fake');
    const count = state.value + 'yoxi';
    count.charAt(1);
  };
</script>
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
const count = useVRef(0);
const foo = useVRef(0);
const state = useReactive({ foo: 'bar', bar: { c: 1 } });

const fn1 = useCallback(() => {
  count.value += state.bar.c;
  console.log(count.value);
}, [count.value, state.bar?.c]);

const fn = () => {};

const fn2 = useCallback(() => {
  const c = foo.value;
  fn();

  const fn4 = () => {
    state.bar.c--;
    c + count.value;
  };
}, [foo.value, state.bar?.c, count.value]);

const fn3 = useCallback(() => {
  foo.value++;

  const state = useVRef('fake');
  const count = state.value + 'yoxi';
  count.charAt(1);
}, [foo.value,]);
Enter fullscreen mode Exit fullscreen mode

This comparison shows:

  • fn1 is identified as a top-level arrow function, collecting count.value and state.bar.c
  • fn2 traces back to c while ignoring the local function fn4
  • fn3 ignores reactive variables created inside the function body, only collecting the external dependency foo.value

Vue composite access and alias tracing

VuReact also traces through complex alias chains and destructured accesses back to their sources.

  • Vue
<script setup lang="ts">
  const objRef = ref({ a: 1, b: { c: 1 } });
  const listRef = ref([1, 2, 3]);
  const aliasA = state.foo;
  const aliasB = aliasA;
  const aliasC = aliasB;
  const { foo: stateFoo } = state;
  const [first] = listRef.value;

  const traceFn = () => {
    aliasC;
  };

  const destructureFn = () => {
    stateFoo;
    first;
  };
</script>
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
const objRef = useVRef({ a: 1, b: { c: 1 } });
const listRef = useVRef([1, 2, 3]);
const aliasA = useMemo(() => state.foo, [state.foo]);
const aliasB = useMemo(() => aliasA, [aliasA]);
const aliasC = useMemo(() => aliasB, [aliasB]);
const { foo: stateFoo } = useMemo(() => state, [state]);
const [first] = useMemo(() => listRef.value, [listRef.value]);

const traceFn = useCallback(() => {
  aliasC;
}, [aliasC]);

const destructureFn = useCallback(() => {
  stateFoo;
  first;
}, [stateFoo, first]);
Enter fullscreen mode Exit fullscreen mode

This shows:

  • Alias chains are resolved layer by layer back to the actual reactive source
  • Destructured variables are also converted into trackable dependencies via useMemo

Vue top-level variable declarations → React useMemo dependency array generation

  • Vue
<script setup lang="ts">
  const fooRef = ref(0);
  const reactiveState = reactive({ foo: 'bar', bar: { c: 1 } });

  const memoizedObj = {
    title: 'test',
    bar: fooRef.value,
    add: () => {
      reactiveState.bar.c++;
    },
  };

  let staticObj = {
    foo: 1,
    state: { bar: { c: 1 } },
  };

  const reactiveList = [fooRef.value, 1, 2];

  const mixedList = [
    { name: reactiveState.foo, age: fooRef.value },
    { name: 'A', age: 20 },
  ];

  const nestedObj = {
    a: {
      b: {
        c: reactiveList[0],
        d: () => {
          return memoizedObj.bar;
        },
      },
      e: mixedList,
    },
  };
</script>
Enter fullscreen mode Exit fullscreen mode
  • Compiled React
const memoizedObj = useMemo(
  () => ({
    title: 'test',
    bar: fooRef.value,
    add: () => {
      reactiveState.bar.c++;
    },
  }),
  [fooRef.value, reactiveState.bar?.c],
);

let staticObj = {
  foo: 1,
  state: {
    bar: {
      c: 1,
    },
  },
};

const reactiveList = useMemo(() => [fooRef.value, 1, 2], [fooRef.value]);

const mixedList = useMemo(
  () => [
    { name: reactiveState.foo, age: fooRef.value },
    { name: 'A', age: 20 },
  ],
  [reactiveState.foo, fooRef.value],
);

const nestedObj = useMemo(
  () => ({
    a: {
      b: {
        c: reactiveList[0],
        d: () => {
          return memoizedObj.bar;
        },
      },
      e: mixedList,
    },
  }),
  [reactiveList[0], memoizedObj.bar, mixedList],
);
Enter fullscreen mode Exit fullscreen mode

The key takeaways here:

  • memoizedObj collects reactive field accesses and method dependencies inside the object
  • staticObj is not optimized into useMemo because it contains no reactive accesses
  • reactiveList, mixedList, and nestedObj have their dependency arrays recursively completed based on their structure

Three Principles of Automatic Dependency Analysis

  1. Only analyze optimizable top-level expressions — local functions and nested scopes are not included in automatic top-level Hook optimization
  2. Follow React's dependency rules — only collect reactive accesses external to the function/variable, not internal local variables
  3. Avoid over-optimization — top-level arrow functions and variables without external reactive dependencies are not forcibly converted into Hooks

Why This Matters

In React, function components recreate top-level functions and variables on every render. If these top-level expressions depend on reactive state and are not stabilized, it can lead to:

  • Unnecessary child component re-renders
  • Frequent Hook recomputations
  • Unpredictable callback identity changes

By automatically generating accurate dependency arrays at compile time, VuReact preserves the conciseness of Vue's syntax while achieving React-side performance optimization.

Related Links

Top comments (0)