Should computed track nested refs?
#14462
Replies: 3 comments 1 reply
-
|
@aeharding Computed properties automatically unwrap top-level return {
component,
outline: outline.value
} |
Beta Was this translation helpful? Give feedback.
-
|
I would not rely on this pattern. A In your example, the getter does all three: const foo = computed(() => {
const num = ref(0);
const modifyPromise = new Promise<void>((resolve) => {
setTimeout(() => {
num.value++;
resolve();
}, 1000);
});
return {
num,
modifyPromise,
};
});That makes the result dependent on when Vue decides to evaluate or re evaluate the computed. In SSR this is especially fragile because server rendering, A safer shape is to make the state stable outside the computed, and use const outline = ref<string[] | null>(null);
const component = shallowRef<Component | null>(null);
const pending = shallowRef<Promise<void> | null>(null);
watchEffect(() => {
const pageModule = modules[route.path]() as Promise<Page>;
component.value = defineAsyncComponent(() => pageModule);
pending.value = pageModule.then((page) => {
outline.value = page.__outline;
});
});
onServerPrefetch(async () => {
await pending.value;
});Or return a single resolved page data object from an async loader and derive from that: const page = shallowRef<Page | null>(null);
async function loadPage(path: string) {
page.value = await modules[path]();
}
watch(
() => route.path,
(path) => {
void loadPage(path);
},
{ immediate: true },
);
onServerPrefetch(async () => {
await loadPage(route.path);
});
const outline = computed(() => page.value?.__outline ?? null);
const component = computed(() =>
page.value
? defineAsyncComponent(() => Promise.resolve(page.value!))
: null,
);The key point is that the ref identity should be stable. The computed can read For your blog use case, I would model it as “route selects a page module, loader resolves the module, computed values read the resolved module.” That gives SSR a concrete promise to wait for and avoids depending on nested refs inside a computed return value. So my answer would be: the inconsistency is not something I would depend on or design around. The recommended fix is to move the async work and mutable refs out of the computed, then use |
Beta Was this translation helpful? Give feedback.
-
|
This is not a bug you should design around — it's undefined behavior being exposed by an unsupported pattern. A Why it behaves differently in SSR vs client: When the inner The difference between 3.5 and 3.6 beta is an internal implementation detail, not a fix. The fix — move async work and mutable state out of the computed: const outline = ref<string[] | null>(null);
const component = shallowRef<Component | null>(null);
const loadPromise = shallowRef<Promise<void>>();
watchEffect(() => {
const pageModule = modules[route.path]() as Promise<Page>;
component.value = defineAsyncComponent(() => pageModule);
loadPromise.value = pageModule.then((page) => {
outline.value = page.__outline;
});
});
onServerPrefetch(() => loadPromise.value);This way:
For your blog, model it as: route selects module → loader resolves it → computed reads the resolved data. Keep the async and the reactive state separate. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
related: #7873, #6994
I found nesting a
refinside acomputedpretty helpful when building my blog:computedthat takes the current route and finds the corresponding pagepage: Promise<Page>, then usedefineAsyncComponentto load it.The
Pagetype is something like:refwhich is initiallynull, and will be updated withpage.then(x => outline.value = x.__outline).As mentioned in #7873 and #6994, this is not the intended usage of the api, but i found it quit useful, and is working as intended on the client side. A minimal reproduction is at https://stackblitz.com/github/illusionaries/ssr-computed, I will also paste the core code here:
But I noticed some inconsistencies between client build and SSR build, even between different versions of vue.
On the client side, regardless of the version, the computed is executed only once, and num is updated from 0 to 1 after 1 second, which is expected.
On the SSR side, with vue latest (3.5.28), the computed got executed twice, and the rendered result is
<span data-v-8338d79c>0</span>. But with vue 3.6 beta (actually, just override"@vue/runtime-core": "beta"is enough), the computed got executed only once, and the rendered result is<span data-v-8338d79c>1</span>.I haven't dug into the source code yet, but I suppose it has something to do with the
computedtracking the internally createdref(at least, the VSCode extension highlights the internalrefas a dependency). On the server side, changing the internalrefvalue caused the computed to evaluate, replacing thefoo.numwith 0 again. But somehow on the client side, therefchange does not trigger an re-evaluation.Is the inconsistency expected or is it actually a bug? If it's expected, is there any recommended way to achieve the same effect without nesting a
refinside acomputed?I have tried to use
defineExposein the page component to expose the outline, but template ref does not work with SSR as far as I know, because the component instance is not created on the server side anduseTemplateRefalways returns null.Beta Was this translation helpful? Give feedback.
All reactions