Skip to content

Commit

Permalink
feat(nuxt3, bridge): add lazy helpers (useLazyAsyncData and `useLaz…
Browse files Browse the repository at this point in the history
…yFetch`) (nuxt#1861)
  • Loading branch information
danielroe authored Nov 15, 2021
1 parent 3e1239d commit f011a60
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 30 deletions.
37 changes: 35 additions & 2 deletions docs/content/1.getting-started/4.bridge-composition-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,42 @@ const wrapProperty = (property, makeComputed = true) => () => {

### `useAsync` and `useFetch`

There is currently no replacement for these two functions with bridge.
These two composables can be replaced with `useLazyAsyncData` and `useLazyFetch`, which are documented [in the Nuxt 3 docs](/docs/usage/data-fetching). Just like the previous `@nuxtjs/composition-api` composables, these composables do not block route navigation on the client-side (hence the 'lazy' part of the name).

You can continue to use `useAsync` and `useFetch` by importing them from `@nuxtjs/composition-api`, which is shimmed by Nuxt Bridge.
::alert
Note that the API is entirely different, despite similar sounding names. Importantly, you should not attempt to change the value of other variables outside the composable (as you may have been doing with the previous `useFetch`).
::

Migrating to the new composables from `useAsync`:

```diff
<script setup>
- import { useAsync } from '@nuxtjs/composition-api'
+ import { useLazyAsyncData, useLazyFetch } from '#app'
- const posts = useAsync(() => $fetch('/api/posts'))
+ const { data: posts } = useLazyAsyncData('posts', () => $fetch('/api/posts'))
+ // or, more simply!
+ const { data: posts } = useLazyFetch('/api/posts')
</script>
```

Migrating to the new composables from `useFetch`:

```diff
<script setup>
- import { useFetch } from '@nuxtjs/composition-api'
+ import { useLazyAsyncData, useLazyFetch } from '#app'
- const posts = ref([])
- const { fetch } = useFetch(() => { posts.value = await $fetch('/api/posts') })
+ const { data: posts, refresh } = useLazyAsyncData('posts', () => $fetch('/api/posts'))
+ // or, more simply!
+ const { data: posts, refresh } = useLazyFetch('/api/posts')
function updatePosts() {
- return fetch()
+ return refresh()
}
</script>
```

### `useMeta`

Expand Down
28 changes: 19 additions & 9 deletions docs/content/3.docs/1.usage/1.data-fetching.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Data Fetching

Nuxt provides `useFetch` and `useAsyncData` to handle data fetching within your application.
Nuxt provides `useFetch`, `useLazyFetch`, `useAsyncData` and `useLazyAsyncData` to handle data fetching within your application.

## `useAsyncData`

Expand All @@ -12,19 +12,20 @@ Within your pages, components and plugins you can use `useAsyncData` to get acce
useAsyncData(
key: string,
fn: () => Object,
options?: { defer: boolean, server: boolean }
options?: { lazy: boolean, server: boolean }
)
```

* **key**: a unique key to ensure that data fetching can be properly de-duplicated across requests
* **fn** an asynchronous function that returns a value.
* **options**:
* _defer_: whether to load the route before resolving the async function (defaults to `false`)
* _server_: whether the fetch the data on server-side (defaults to `true`)
* _lazy_: whether to resolve the async function after loading the route, instead of blocking navigation (defaults to `false`)
* _default_: a factory function to set the default value of the data, before the async function resolves - particularly useful with the `lazy: true` option
* _server_: whether to fetch the data on server-side (defaults to `true`)
* _transform_: A function that can be used to alter fn result after resolving
* _pick_: Only pick specified keys in this array from fn result

Under the hood, `defer: false` uses `<Suspense>` to block the loading of the route before the data has been fetched. Consider using `defer: true` and implementing a loading state instead for a snappier user experience.
Under the hood, `lazy: false` uses `<Suspense>` to block the loading of the route before the data has been fetched. Consider using `lazy: true` and implementing a loading state instead for a snappier user experience.

### Example

Expand All @@ -46,6 +47,10 @@ const { data } = await useAsyncData('count', () => $fetch('/api/count'))
</template>
```

## `useLazyAsyncData`

This composable behaves identically to `useAsyncData` with the `lazy: true` option set. In other words, the async function does not block navigation. That means you will need to handle the situation where the data is `null` (or whatever value you have provided in a custom `default` factory function).

## `useFetch`

Within your pages, components and plugins you can use `useFetch` to get universally fetch from any URL.
Expand All @@ -70,8 +75,9 @@ Available options:
* `params`: Query params
* `baseURL`: Base URL for request
* Options from `useAsyncData`
* `defer`
* `lazy`
* `server`
* `default`
* `pick`
* `transform`
Expand All @@ -87,9 +93,13 @@ const { data } = await useFetch('/api/count')
</template>
```
### Best practices
## `useLazyFetch`
This composable behaves identically to `useFetch` with the `lazy: true` option set. In other words, the async function does not block navigation. That means you will need to handle the situation where the data is `null` (or whatever value you have provided in a custom `default` factory function).
## Best practices
The data returned by `useAsyncData` will be stored inside the page payload. This means that every key returned that is not used in your component will be added to the payload.
The data returned by these composables will be stored inside the page payload. This means that every key returned that is not used in your component will be added to the payload.
::alert{icon=👉}
**We strongly recommend you only select the keys that you will use in your component.**
Expand Down Expand Up @@ -124,7 +134,7 @@ const { data: mountain } = await useFetch('/api/mountains/everest', { pick: ['ti
</template>
```
### Using async setup
## Using async setup
If you are using `async setup()`, the current component instance will be lost after the first `await`. (This is a Vue 3 limitation.) If you want to use multiple async operations, such as multiple calls to `useFetch`, you will need to use `<script setup>` or await them together at the end of setup.
Expand Down
1 change: 1 addition & 0 deletions packages/bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"hash-sum": "^2.0.0",
"magic-string": "^0.25.7",
"mlly": "^0.3.13",
"murmurhash-es": "^0.1.1",
"node-fetch": "^3.1.0",
"nuxi": "3.0.0",
"p-debounce": "^4.0.0",
Expand Down
19 changes: 18 additions & 1 deletion packages/bridge/src/runtime/app.plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import Vue from 'vue'
import { createHooks } from 'hookable'
import { setNuxtAppInstance } from '#app'

// Reshape payload to match key `useLazyAsyncData` expects
function proxiedState (state) {
state._asyncData = state._asyncData || {}
return new Proxy(state, {
get (target, prop) {
if (prop === 'data') {
return target._asyncData
}
if (prop === '_data') {
return target.state
}
return Reflect.get(target, prop)
}
})
}

export default (ctx, inject) => {
const nuxtApp = {
vueApp: {
Expand All @@ -21,7 +37,8 @@ export default (ctx, inject) => {
},
provide: inject,
globalName: 'nuxt',
payload: process.client ? ctx.nuxtState : ctx.ssrContext.nuxt,
payload: proxiedState(process.client ? ctx.nuxtState : ctx.ssrContext.nuxt),
_asyncDataPromises: [],
isHydrating: ctx.isHMR,
nuxt2Context: ctx
}
Expand Down
1 change: 1 addition & 0 deletions packages/bridge/src/runtime/asyncData.ts
4 changes: 4 additions & 0 deletions packages/bridge/src/runtime/composables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import type { Route } from 'vue-router'
import type { RuntimeConfig } from '@nuxt/kit'
import { useNuxtApp } from './app'

export { useLazyAsyncData } from './asyncData'
export { useLazyFetch } from './fetch'

export * from '@vue/composition-api'

const mock = () => () => { throw new Error('not implemented') }

export const useAsyncData = mock()
export const useFetch = mock()
export const useHydration = mock()

// Runtime config helper
Expand Down
1 change: 1 addition & 0 deletions packages/bridge/src/runtime/fetch.ts
7 changes: 7 additions & 0 deletions packages/bridge/src/runtime/vue2-bridge.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Vue from 'vue'

export * from '@vue/composition-api'

export declare const isFunction: (fn: unknown) => boolean

export { Vue as default }
38 changes: 28 additions & 10 deletions packages/nuxt3/src/app/composables/asyncData.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { onBeforeMount, onUnmounted, ref, getCurrentInstance } from 'vue'
import { onBeforeMount, onServerPrefetch, onUnmounted, ref, getCurrentInstance } from 'vue'
import type { Ref } from 'vue'
import { NuxtApp, useNuxtApp } from '#app'

export type _Transform<Input=any, Output=any> = (input: Input) => Output
export type _Transform<Input = any, Output = any> = (input: Input) => Output

export type PickFrom<T, K extends Array<string>> = T extends Array<any> ? T : T extends Record<string, any> ? Pick<T, K[number]> : T
export type KeysOf<T> = Array<keyof T extends string ? keyof T : string>
Expand All @@ -14,7 +14,7 @@ export interface AsyncDataOptions<
PickKeys extends KeyOfRes<_Transform> = KeyOfRes<Transform>
> {
server?: boolean
defer?: boolean
lazy?: boolean
default?: () => DataT
transform?: Transform
pick?: PickKeys
Expand All @@ -39,7 +39,7 @@ export function useAsyncData<
key: string,
handler: (ctx?: NuxtApp) => Promise<DataT>,
options: AsyncDataOptions<DataT, Transform, PickKeys> = {}
) : AsyncData<PickFrom<ReturnType<Transform>, PickKeys>> {
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>> {
// Validate arguments
if (typeof key !== 'string') {
throw new TypeError('asyncData key must be a string')
Expand All @@ -49,7 +49,12 @@ export function useAsyncData<
}

// Apply defaults
options = { server: true, defer: false, default: getDefault, ...options }
options = { server: true, default: getDefault, ...options }
// TODO: remove support for `defer` in Nuxt 3 RC
if ((options as any).defer) {
console.warn('[useAsyncData] `defer` has been renamed to `lazy`. Support for `defer` will be removed in RC.')
}
options.lazy = options.lazy ?? (options as any).defer ?? false

// Setup nuxt instance payload
const nuxt = useNuxtApp()
Expand Down Expand Up @@ -109,7 +114,8 @@ export function useAsyncData<

// Server side
if (process.server && fetchOnServer) {
asyncData.refresh()
const promise = asyncData.refresh()
onServerPrefetch(() => promise)
}

// Client side
Expand All @@ -120,14 +126,14 @@ export function useAsyncData<
}
// 2. Initial load (server: false): fetch on mounted
if (nuxt.isHydrating && clientOnly) {
// Fetch on mounted (initial load or deferred fetch)
// Fetch on mounted (initial load or lazy fetch)
instance._nuxtOnBeforeMountCbs.push(asyncData.refresh)
} else if (!nuxt.isHydrating) { // Navigation
if (options.defer) {
// 3. Navigation (defer: true): fetch on mounted
if (options.lazy) {
// 3. Navigation (lazy: true): fetch on mounted
instance._nuxtOnBeforeMountCbs.push(asyncData.refresh)
} else {
// 4. Navigation (defer: false): await fetch
// 4. Navigation (lazy: false): await fetch
asyncData.refresh()
}
}
Expand All @@ -141,6 +147,18 @@ export function useAsyncData<
return asyncDataPromise as AsyncData<DataT>
}

export function useLazyAsyncData<
DataT,
Transform extends _Transform<DataT> = _Transform<DataT, DataT>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
> (
key: string,
handler: (ctx?: NuxtApp) => Promise<DataT>,
options: Omit<AsyncDataOptions<DataT, Transform, PickKeys>, 'lazy'> = {}
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>> {
return useAsyncData(key, handler, { ...options, lazy: true })
}

function pick (obj: Record<string, any>, keys: string[]) {
const newObj = {}
for (const key of keys) {
Expand Down
22 changes: 17 additions & 5 deletions packages/nuxt3/src/app/composables/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ export type UseFetchOptions<
> = AsyncDataOptions<DataT, Transform, PickKeys> & FetchOptions & { key?: string }

export function useFetch<
ReqT extends string = string,
ResT = FetchResult<ReqT>,
Transform extends (res: ResT) => any = (res: ResT) => ResT,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
> (
ReqT extends string = string,
ResT = FetchResult<ReqT>,
Transform extends (res: ResT) => any = (res: ResT) => ResT,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
> (
url: ReqT,
opts: UseFetchOptions<ResT, Transform, PickKeys> = {}
) {
Expand All @@ -38,6 +38,18 @@ export function useFetch<
return useAsyncData(opts.key, () => $fetch(url, opts) as Promise<ResT>, opts)
}

export function useLazyFetch<
ReqT extends string = string,
ResT = FetchResult<ReqT>,
Transform extends (res: ResT) => any = (res: ResT) => ResT,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
> (
url: ReqT,
opts: Omit<UseFetchOptions<ResT, Transform, PickKeys>, 'lazy'> = {}
) {
return useFetch(url, { ...opts, lazy: true })
}

function generateKey (keys) {
return '$f' + murmurHashV3(JSON.stringify(keys))
}
4 changes: 2 additions & 2 deletions packages/nuxt3/src/app/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { defineNuxtComponent } from './component'
export { useAsyncData } from './asyncData'
export { useAsyncData, useLazyAsyncData } from './asyncData'
export { useHydration } from './hydrate'
export { useState } from './state'
export { useFetch } from './fetch'
export { useFetch, useLazyFetch } from './fetch'
4 changes: 3 additions & 1 deletion packages/nuxt3/src/auto-imports/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ export const Nuxt3AutoImports: AutoImportSource[] = [
from: '#app',
names: [
'useAsyncData',
'useLazyAsyncData',
'defineNuxtComponent',
'useNuxtApp',
'defineNuxtPlugin',
'useRuntimeConfig',
'useState',
'useFetch'
'useFetch',
'useLazyFetch'
]
},
// #meta
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2445,6 +2445,7 @@ __metadata:
hash-sum: ^2.0.0
magic-string: ^0.25.7
mlly: ^0.3.13
murmurhash-es: ^0.1.1
node-fetch: ^3.1.0
nuxi: 3.0.0
nuxt: ^2
Expand Down

0 comments on commit f011a60

Please sign in to comment.