Skip to content

Commit

Permalink
feat(ssr): support SSR
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Aug 10, 2022
1 parent e3b6215 commit 5578f7d
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 24 deletions.
6 changes: 4 additions & 2 deletions src/data-fetching/dataCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,16 @@ export function isCacheExpired(
}

export function createDataCacheEntry<T, isLazy extends boolean = boolean>(
options: Required<DefineLoaderOptions<isLazy>>
options: Required<DefineLoaderOptions<isLazy>>,
initialState?: T
): DataLoaderCacheEntry<T, isLazy> {
return withinScope<DataLoaderCacheEntry<T, isLazy>>(() => ({
pending: ref(false),
error: ref<any>(),
when: Date.now(),
loaders: new Set(),
// @ts-expect-error: data always start as empty
data: ref(),
data: ref(initialState),
params: {},
query: {},
// hash: null,
Expand Down Expand Up @@ -124,6 +125,7 @@ export let currentContext:
| [DataLoaderCacheEntry, Router, RouteLocationNormalizedLoaded]
| undefined
| null

export function getCurrentContext() {
return currentContext || ([] as const)
}
Expand Down
52 changes: 36 additions & 16 deletions src/data-fetching/dataFetchingGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,26 @@ declare module 'vue-router' {
}
}

export interface DataFetchingOptions {
/**
* If true, fetching won't block the navigation. If a number is passed, the fetching will block that many milliseconds
* before letting the navigation continue.
*/
lazy?: boolean | number | (() => boolean | number)
}

// dev only check
let added: boolean = false
const ADDED_SYMBOL = Symbol()

export function setupDataFetchingGuard(router: Router) {
export function setupDataFetchingGuard(
router: Router,
initialState?: Record<string, unknown>
) {
// TODO: dev only
if (added) {
if (ADDED_SYMBOL in router) {
console.warn(
'[vue-router]: Data fetching guard added twice. Make sure to remove the extra call.'
)
return
}
added = true
return router.beforeEach((to) => {
// @ts-expect-error: doesn't exist
router[ADDED_SYMBOL] = true

const fetchedState: Record<string, unknown> = {}

router.beforeEach((to) => {
// We run all loaders in parallel
return (
Promise.all(
Expand All @@ -59,14 +58,35 @@ export function setupDataFetchingGuard(router: Router) {
return Promise.all(
// load will ensure only one request is happening at a time
loaders.map((loader) => {
return loader._.load(to, router)
const {
options: { key },
cache,
} = loader._
return loader._.load(
to,
router,
undefined,
initialState
).then(() => {
if (!initialState) {
// TODO: warn if we have an incomplete initialState
if (key) {
fetchedState[key] = cache.get(router)!.data.value
}
}
})
})
)
})
)
)
// let the navigation go through
.then(() => true)
// let the navigation go through by returning true or void
.then(() => {
// reset the initial state as it can only be used once
initialState = undefined
})
)
})

return initialState ? null : fetchedState
}
29 changes: 29 additions & 0 deletions src/data-fetching/defineLoader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,35 @@ describe('defineLoader', () => {
expect(spy).toHaveBeenCalledTimes(3)
})
})

describe('ssr initialState', () => {
it('skips the load function if there is an initial state', async () => {
const spy = vi.fn().mockResolvedValue({ name: 'edu' })
const useLoader = defineLoader(spy, { key: 'id' })

await useLoader._.load(route, router, undefined, { id: { name: 'edu' } })
expect(spy).toHaveBeenCalledTimes(0)
})

it('always calls the loader after hydration', async () => {
const spy = vi.fn().mockResolvedValue({ name: 'edu' })
const useLoader = defineLoader(spy, { key: 'id' })

await useLoader._.load(route, router, undefined, { id: { name: 'edu' } })
await useLoader._.load(route, router)
expect(spy).toHaveBeenCalledTimes(1)
})

it('calls the loader with refresh', async () => {
const spy = vi.fn().mockResolvedValue({ name: 'edu' })
const useLoader = defineLoader(spy, { key: 'id' })

await useLoader._.load(route, router, undefined, { id: { name: 'edu' } })
const { refresh } = useLoader()
await refresh()
expect(spy).toHaveBeenCalledTimes(1)
})
})
})

// dts testing
Expand Down
40 changes: 34 additions & 6 deletions src/data-fetching/defineLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@ export interface DefineLoaderOptions<isLazy extends boolean = boolean> {
* instead of all the individual properties returned by the loader.
*/
lazy?: isLazy

/**
* SSR Key to store the data in an object that can be serialized later to the HTML page.
*/
key?: string
}

const DEFAULT_DEFINE_LOADER_OPTIONS: Required<DefineLoaderOptions> = {
cacheTime: 1000 * 5,
lazy: false,
key: '',
// cacheTime: 1000 * 60 * 5,
}

Expand Down Expand Up @@ -163,17 +169,28 @@ export function defineLoader<P extends Promise<any>, isLazy extends boolean>(
function load(
route: RouteLocationNormalizedLoaded,
router: Router,
parent?: DataLoaderCacheEntry
parent?: DataLoaderCacheEntry,
initialState?: Record<string, unknown>
): Promise<void> {
const hasCacheEntry = cache.has(router)
const needsNewLoad =
!hasCacheEntry || shouldFetchAgain(cache.get(router)!, route)

const initialData =
initialState && (initialState[options.key] as Awaited<P>)

if (!hasCacheEntry) {
cache.set(router, createDataCacheEntry(options))
cache.set(router, createDataCacheEntry(options, initialData))
}

const entry = cache.get(router)!

if (initialData) {
// invalidate the entry because we don't have the params it was created with
entry.when = 0
return Promise.resolve()
}

const needsNewLoad = !hasCacheEntry || shouldFetchAgain(entry, route)

const { isReady, pending, error } = entry
const { lazy } = options

Expand Down Expand Up @@ -235,7 +252,7 @@ export function defineLoader<P extends Promise<any>, isLazy extends boolean>(
}

// NOTE: unfortunately we need to duplicate this part here and on the `finally()` above
// to handle all
// to handle different call scenarios
setCurrentContext(parent && [parent, router, route])
}))
}
Expand All @@ -252,6 +269,7 @@ export function defineLoader<P extends Promise<any>, isLazy extends boolean>(
loader,
cache,
load,
options,
}
dataLoader[IsLoader] = true

Expand Down Expand Up @@ -341,13 +359,23 @@ export interface _DataLoaderInternals<T> {
/**
* Loads the data from the cache if possible, otherwise loads it from the loader and awaits it.
*/
load: (route: RouteLocationNormalizedLoaded, router: Router) => Promise<void>
load: (
route: RouteLocationNormalizedLoaded,
router: Router,
parent?: DataLoaderCacheEntry,
initialState?: Record<string, unknown>
) => Promise<void>

/**
* The data loaded by the loader associated with the router instance. As one router instance can only be used for one
* app, it ensures the cache is not shared among requests.
*/
cache: WeakMap<Router, DataLoaderCacheEntry<T>>

/**
* Resolved options for the loader.
*/
options: Required<DefineLoaderOptions>
}

export interface _DataLoaderResult<T = unknown, isLazy = boolean> {
Expand Down

0 comments on commit 5578f7d

Please sign in to comment.