Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

fix(nuxt): use shared state for asyncData #7055

Merged
merged 2 commits into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions packages/nuxt/src/app/composables/asyncData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,16 @@ export function useAsyncData<

const useInitialCache = () => (nuxt.isHydrating || options.initialCache) && nuxt.payload.data[key] !== undefined

const asyncData = {
data: ref(useInitialCache() ? nuxt.payload.data[key] : options.default?.() ?? null),
pending: ref(!useInitialCache()),
error: ref(nuxt.payload._errors[key] ?? null)
} as AsyncData<DataT, DataE>
// Create or use a shared asyncData entity
if (!nuxt._asyncData[key]) {
nuxt._asyncData[key] = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pi0 Have you checked whether this retains reactivity if this is created outside a component setup (eg in plugin), or when it is used after the original component that created it has unmounted?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can try it (as mentioned in the description we need to iterate over to fix the related issue).

BTW really the use case of useAsyncData is to be used within the component lifecycle, not the plugins even if partially possible.

Good point about reactivity after unmounting. Will test this.

data: ref(useInitialCache() ? nuxt.payload.data[key] : options.default?.() ?? null),
pending: ref(!useInitialCache()),
error: ref(nuxt.payload._errors[key] ?? null)
}
}
// TODO: Else, Soemhow check for confliciting keys with different defaults or fetcher
const asyncData = { ...nuxt._asyncData[key] } as AsyncData<DataT, DataE>

asyncData.refresh = (opts = {}) => {
// Avoid fetching same key more than once at a time
Expand Down
8 changes: 7 additions & 1 deletion packages/nuxt/src/app/nuxt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-use-before-define */
import { getCurrentInstance, reactive } from 'vue'
import { getCurrentInstance, reactive, Ref } from 'vue'
import type { App, onErrorCaptured, VNode } from 'vue'
import { createHooks, Hookable } from 'hookable'
import type { RuntimeConfig, AppConfigInput } from '@nuxt/schema'
Expand Down Expand Up @@ -66,6 +66,11 @@ interface _NuxtApp {
[key: string]: any

_asyncDataPromises: Record<string, Promise<any> | undefined>
_asyncData: Record<string, {
data: Ref<any>
pending: Ref<boolean>
error: Ref<any>
}>,

ssrContext?: NuxtSSRContext
payload: {
Expand Down Expand Up @@ -113,6 +118,7 @@ export function createNuxtApp (options: CreateOptions) {
}),
isHydrating: process.client,
_asyncDataPromises: {},
_asyncData: {},
...options
} as any as NuxtApp

Expand Down
18 changes: 18 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,21 @@ describe('app config', () => {
expect(html).toContain(JSON.stringify(expectedAppConfig))
})
})

describe('useAsyncData', () => {
it('single request resolves', async () => {
await expectNoClientErrors('/useAsyncData/single')
})

it('two requests resolve', async () => {
await expectNoClientErrors('/useAsyncData/double')
})

it('two requests resolve and sync', async () => {
await $fetch('/useAsyncData/refresh')
})

it('two requests made at once resolve and sync', async () => {
await expectNoClientErrors('/useAsyncData/promise-all')
})
})
7 changes: 7 additions & 0 deletions test/fixtures/basic/composables/asyncDataTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const useSleep = () => useAsyncData('sleep', async () => {
await new Promise(resolve => setTimeout(resolve, 50))

return 'Slept!'
})

export const useCounter = () => useFetch('/api/useAsyncData/count')
26 changes: 26 additions & 0 deletions test/fixtures/basic/pages/useAsyncData/double.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template>
<div>
Single
<div>
data1: {{ data1 }}
data2: {{ data2 }}
</div>
</div>
</template>

<script setup lang="ts">
const { data: data1 } = await useSleep()
const { data: data2 } = await useSleep()

if (data1.value === null || data1.value === undefined || data1.value.length <= 0) {
throw new Error('Data should never be null or empty.')
}

if (data2.value === null || data2.value === undefined || data2.value.length <= 0) {
throw new Error('Data should never be null or empty.')
}

if (data1.value !== data2.value) {
throw new Error('AsyncData not synchronised')
}
</script>
31 changes: 31 additions & 0 deletions test/fixtures/basic/pages/useAsyncData/promise-all.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<div>
Single
<div>
data1: {{ result1.data.value }}
data2: {{ result2.data.value }}
</div>
</div>
</template>

<script setup lang="ts">
const [result1, result2] = await Promise.all([useSleep(), useSleep()])

if (result1.data.value === null || result1.data.value === undefined || result1.data.value.length <= 0) {
throw new Error('Data should never be null or empty.')
}

if (result2.data.value === null || result2.data.value === undefined || result2.data.value.length <= 0) {
throw new Error('Data should never be null or empty.')
}

if (result1.data.value !== result2.data.value) {
throw new Error('AsyncData not synchronised')
}

await result1.refresh()

if (result1.data.value !== result2.data.value) {
throw new Error('AsyncData not synchronised')
}
</script>
39 changes: 39 additions & 0 deletions test/fixtures/basic/pages/useAsyncData/refresh.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<div>
Single
<div>
{{ data }} - {{ data2 }}
</div>
</div>
</template>

<script setup lang="ts">
const { data, refresh } = await useCounter()
const { data: data2, refresh: refresh2 } = await useCounter()

let inital = data.value.count

// Refresh on client and server side
await refresh()

if (data.value.count !== inital + 1) {
throw new Error('Data not refreshed?' + data.value.count + ' : ' + data2.value.count)
}

if (data.value.count !== data2.value.count) {
throw new Error('AsyncData not synchronised')
}

inital = data.value.count

await refresh2()

if (data.value.count !== inital + 1) {
throw new Error('data2 refresh not syncronised?')
}

if (data.value.count !== data2.value.count) {
throw new Error('AsyncData not synchronised')
}

</script>
16 changes: 16 additions & 0 deletions test/fixtures/basic/pages/useAsyncData/single.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<div>
Single
<div>
{{ data }}
</div>
</div>
</template>

<script setup lang="ts">
const { data } = await useSleep()

if (data.value === null || data.value === undefined || data.value.length <= 0) {
throw new Error('Data should never be null or empty.')
}
</script>
3 changes: 3 additions & 0 deletions test/fixtures/basic/server/api/useAsyncData/count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
let counter = 0

export default () => ({ count: counter++ })