vize:croquis/cf/async-boundary
Reactive state crosses an async boundary and can be observed stale.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
lint: {
vize: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/async-boundary": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./SearchPage.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
api.ts
export interface Result { items: string[]; }
export async function load(query: string, options?: { signal?: AbortSignal }): Promise<Result> {
const response = await fetch(`/search?q=${encodeURIComponent(query)}`, options);
return response.json();
}
Bad
A slower old query can finish after a newer query and overwrite result, because the watcher has no invalidation cleanup.
SearchPage.vue
<script setup lang="ts">
import { ref } from "vue";
import SearchResults from "./SearchResults.vue";
const query = ref("");
</script>
<template>
<SearchResults :query="query" />
</template>
SearchResults.vue
<script setup lang="ts">
import { load, type Result } from "./api";
import { ref, watch } from "vue";
const props = defineProps<{ query: string }>();
const result = ref<Result | null>(null);
watch(
() => props.query,
async (value) => {
result.value = await load(value);
},
);
</script>
Good
Register cleanup before awaiting: abort the old request and invalidate its active flag, then assign only a still-active response.
SearchPage.vue
<script setup lang="ts">
import { ref } from "vue";
import SearchResults from "./SearchResults.vue";
const query = ref("");
</script>
<template>
<SearchResults :query="query" />
</template>
SearchResults.vue
<script setup lang="ts">
import { load, type Result } from "./api";
import { ref, watch } from "vue";
const props = defineProps<{ query: string }>();
const result = ref<Result | null>(null);
watch(
() => props.query,
async (value, _oldValue, onCleanup) => {
const controller = new AbortController();
let active = true;
onCleanup(() => {
active = false;
controller.abort();
});
const next = await load(value, { signal: controller.signal });
if (active) result.value = next;
},
);
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.