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:
- All Vue and React snippets focus on core logic only, with full component wrappers and unrelated configuration omitted.
- 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>
- 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,]);
This comparison shows:
-
fn1is identified as a top-level arrow function, collectingcount.valueandstate.bar.c -
fn2traces back tocwhile ignoring the local functionfn4 -
fn3ignores reactive variables created inside the function body, only collecting the external dependencyfoo.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>
- 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]);
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>
- 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],
);
The key takeaways here:
-
memoizedObjcollects reactive field accesses and method dependencies inside the object -
staticObjis not optimized intouseMemobecause it contains no reactive accesses -
reactiveList,mixedList, andnestedObjhave their dependency arrays recursively completed based on their structure
Three Principles of Automatic Dependency Analysis
- Only analyze optimizable top-level expressions — local functions and nested scopes are not included in automatic top-level Hook optimization
- Follow React's dependency rules — only collect reactive accesses external to the function/variable, not internal local variables
- 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.
Top comments (0)