fix: ensure mpa navigation render side effects are only fired once (#55032)
This is to fix an issue where these redirect side effects can be fired multiple times when the router reducer state changes. This block is still run when the router state updates, which can lead to superfluous attempts to redirect to a page. With these changes, we keep track of the page that is being redirected to. If a re-render occurs while that request is in flight, we don't trigger the side effects. [Slack x-ref](https://vercel.slack.com/archives/C04DUD7EB1B/p1694049914264839)
This commit is contained in:
parent
068002bb3a
commit
d330f7b02c
8 changed files with 134 additions and 34 deletions
|
@ -33,12 +33,12 @@ import {
|
||||||
ACTION_RESTORE,
|
ACTION_RESTORE,
|
||||||
ACTION_SERVER_ACTION,
|
ACTION_SERVER_ACTION,
|
||||||
ACTION_SERVER_PATCH,
|
ACTION_SERVER_PATCH,
|
||||||
|
Mutable,
|
||||||
PrefetchKind,
|
PrefetchKind,
|
||||||
ReducerActions,
|
ReducerActions,
|
||||||
RouterChangeByServerResponse,
|
RouterChangeByServerResponse,
|
||||||
RouterNavigate,
|
RouterNavigate,
|
||||||
ServerActionDispatcher,
|
ServerActionDispatcher,
|
||||||
ServerActionMutable,
|
|
||||||
} from './router-reducer/router-reducer-types'
|
} from './router-reducer/router-reducer-types'
|
||||||
import { createHrefFromUrl } from './router-reducer/create-href-from-url'
|
import { createHrefFromUrl } from './router-reducer/create-href-from-url'
|
||||||
import {
|
import {
|
||||||
|
@ -73,7 +73,7 @@ export function getServerActionDispatcher() {
|
||||||
return globalServerActionDispatcher
|
return globalServerActionDispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
let globalServerActionMutable: ServerActionMutable['globalMutable'] = {
|
let globalMutable: Mutable['globalMutable'] = {
|
||||||
refresh: () => {}, // noop until the router is initialized
|
refresh: () => {}, // noop until the router is initialized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ function useServerActionDispatcher(dispatch: React.Dispatch<ReducerActions>) {
|
||||||
dispatch({
|
dispatch({
|
||||||
...actionPayload,
|
...actionPayload,
|
||||||
type: ACTION_SERVER_ACTION,
|
type: ACTION_SERVER_ACTION,
|
||||||
mutable: { globalMutable: globalServerActionMutable },
|
mutable: { globalMutable },
|
||||||
cache: createEmptyCacheNode(),
|
cache: createEmptyCacheNode(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -174,7 +174,7 @@ function useChangeByServerResponse(
|
||||||
previousTree,
|
previousTree,
|
||||||
overrideCanonicalUrl,
|
overrideCanonicalUrl,
|
||||||
cache: createEmptyCacheNode(),
|
cache: createEmptyCacheNode(),
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -186,7 +186,7 @@ function useNavigate(dispatch: React.Dispatch<ReducerActions>): RouterNavigate {
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(href, navigateType, forceOptimisticNavigation, shouldScroll) => {
|
(href, navigateType, forceOptimisticNavigation, shouldScroll) => {
|
||||||
const url = new URL(addBasePath(href), location.href)
|
const url = new URL(addBasePath(href), location.href)
|
||||||
globalServerActionMutable.pendingNavigatePath = href
|
globalMutable.pendingNavigatePath = href
|
||||||
|
|
||||||
return dispatch({
|
return dispatch({
|
||||||
type: ACTION_NAVIGATE,
|
type: ACTION_NAVIGATE,
|
||||||
|
@ -197,7 +197,7 @@ function useNavigate(dispatch: React.Dispatch<ReducerActions>): RouterNavigate {
|
||||||
shouldScroll: shouldScroll ?? true,
|
shouldScroll: shouldScroll ?? true,
|
||||||
navigateType,
|
navigateType,
|
||||||
cache: createEmptyCacheNode(),
|
cache: createEmptyCacheNode(),
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
@ -322,7 +322,7 @@ function Router({
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTION_REFRESH,
|
type: ACTION_REFRESH,
|
||||||
cache: createEmptyCacheNode(),
|
cache: createEmptyCacheNode(),
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -338,7 +338,7 @@ function Router({
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTION_FAST_REFRESH,
|
type: ACTION_FAST_REFRESH,
|
||||||
cache: createEmptyCacheNode(),
|
cache: createEmptyCacheNode(),
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -357,7 +357,7 @@ function Router({
|
||||||
}, [appRouter])
|
}, [appRouter])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
globalServerActionMutable.refresh = appRouter.refresh
|
globalMutable.refresh = appRouter.refresh
|
||||||
}, [appRouter.refresh])
|
}, [appRouter.refresh])
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
@ -409,12 +409,17 @@ function Router({
|
||||||
// in <Offscreen>. At least I hope so. (It will run twice in dev strict mode,
|
// in <Offscreen>. At least I hope so. (It will run twice in dev strict mode,
|
||||||
// but that's... fine?)
|
// but that's... fine?)
|
||||||
if (pushRef.mpaNavigation) {
|
if (pushRef.mpaNavigation) {
|
||||||
|
// if there's a re-render, we don't want to trigger another redirect if one is already in flight to the same URL
|
||||||
|
if (globalMutable.pendingMpaPath !== canonicalUrl) {
|
||||||
const location = window.location
|
const location = window.location
|
||||||
if (pushRef.pendingPush) {
|
if (pushRef.pendingPush) {
|
||||||
location.assign(canonicalUrl)
|
location.assign(canonicalUrl)
|
||||||
} else {
|
} else {
|
||||||
location.replace(canonicalUrl)
|
location.replace(canonicalUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalMutable.pendingMpaPath = canonicalUrl
|
||||||
|
}
|
||||||
// TODO-APP: Should we listen to navigateerror here to catch failed
|
// TODO-APP: Should we listen to navigateerror here to catch failed
|
||||||
// navigations somehow? And should we call window.stop() if a SPA navigation
|
// navigations somehow? And should we call window.stop() if a SPA navigation
|
||||||
// should interrupt an MPA one?
|
// should interrupt an MPA one?
|
||||||
|
|
|
@ -106,6 +106,10 @@ const getInitialRouterStateTree = (): FlightRouterState => [
|
||||||
true,
|
true,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const globalMutable = {
|
||||||
|
refresh: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
async function runPromiseThrowChain(fn: any): Promise<any> {
|
async function runPromiseThrowChain(fn: any): Promise<any> {
|
||||||
try {
|
try {
|
||||||
return await fn()
|
return await fn()
|
||||||
|
@ -194,7 +198,7 @@ describe('navigateReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = await runPromiseThrowChain(() =>
|
const newState = await runPromiseThrowChain(() =>
|
||||||
|
@ -438,7 +442,7 @@ describe('navigateReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
await runPromiseThrowChain(() => navigateReducer(state, action))
|
await runPromiseThrowChain(() => navigateReducer(state, action))
|
||||||
|
@ -633,7 +637,7 @@ describe('navigateReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
await runPromiseThrowChain(() => navigateReducer(state, action))
|
await runPromiseThrowChain(() => navigateReducer(state, action))
|
||||||
|
@ -792,7 +796,7 @@ describe('navigateReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
await runPromiseThrowChain(() => navigateReducer(state, action))
|
await runPromiseThrowChain(() => navigateReducer(state, action))
|
||||||
|
@ -948,7 +952,7 @@ describe('navigateReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
await runPromiseThrowChain(() => navigateReducer(state, action))
|
await runPromiseThrowChain(() => navigateReducer(state, action))
|
||||||
|
@ -1147,7 +1151,7 @@ describe('navigateReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
await runPromiseThrowChain(() => navigateReducer(state, action))
|
await runPromiseThrowChain(() => navigateReducer(state, action))
|
||||||
|
@ -1317,7 +1321,7 @@ describe('navigateReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
await runPromiseThrowChain(() => navigateReducer(state, action))
|
await runPromiseThrowChain(() => navigateReducer(state, action))
|
||||||
|
@ -1630,7 +1634,7 @@ describe('navigateReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
await runPromiseThrowChain(() => navigateReducer(state, action))
|
await runPromiseThrowChain(() => navigateReducer(state, action))
|
||||||
|
@ -1841,6 +1845,7 @@ describe('navigateReducer', () => {
|
||||||
hashFragment: '#hash',
|
hashFragment: '#hash',
|
||||||
pendingPush: true,
|
pendingPush: true,
|
||||||
shouldScroll: true,
|
shouldScroll: true,
|
||||||
|
globalMutable,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1983,7 +1988,7 @@ describe('navigateReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = await runPromiseThrowChain(() =>
|
const newState = await runPromiseThrowChain(() =>
|
||||||
|
|
|
@ -66,6 +66,10 @@ const getInitialRouterStateTree = (): FlightRouterState => [
|
||||||
true,
|
true,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const globalMutable = {
|
||||||
|
refresh: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
async function runPromiseThrowChain(fn: any): Promise<any> {
|
async function runPromiseThrowChain(fn: any): Promise<any> {
|
||||||
try {
|
try {
|
||||||
return await fn()
|
return await fn()
|
||||||
|
@ -139,7 +143,7 @@ describe('refreshReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
origin: new URL('/linking', 'https://localhost').origin,
|
origin: new URL('/linking', 'https://localhost').origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,7 +304,7 @@ describe('refreshReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
origin: new URL('/linking', 'https://localhost').origin,
|
origin: new URL('/linking', 'https://localhost').origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -487,7 +491,7 @@ describe('refreshReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
origin: new URL('/linking', 'https://localhost').origin,
|
origin: new URL('/linking', 'https://localhost').origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,7 +727,7 @@ describe('refreshReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
origin: new URL('/linking', 'https://localhost').origin,
|
origin: new URL('/linking', 'https://localhost').origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,10 @@ import type {
|
||||||
|
|
||||||
const buildId = 'development'
|
const buildId = 'development'
|
||||||
|
|
||||||
|
const globalMutable = {
|
||||||
|
refresh: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
jest.mock('../fetch-server-response', () => {
|
jest.mock('../fetch-server-response', () => {
|
||||||
const flightData: FlightData = [
|
const flightData: FlightData = [
|
||||||
[
|
[
|
||||||
|
@ -184,7 +188,7 @@ describe('serverPatchReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = await runPromiseThrowChain(() =>
|
const newState = await runPromiseThrowChain(() =>
|
||||||
|
@ -375,7 +379,7 @@ describe('serverPatchReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
await runPromiseThrowChain(() => serverPatchReducer(state, action))
|
await runPromiseThrowChain(() => serverPatchReducer(state, action))
|
||||||
|
@ -514,7 +518,7 @@ describe('serverPatchReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = createInitialRouterState({
|
const state = createInitialRouterState({
|
||||||
|
@ -556,7 +560,7 @@ describe('serverPatchReducer', () => {
|
||||||
subTreeData: null,
|
subTreeData: null,
|
||||||
parallelRoutes: new Map(),
|
parallelRoutes: new Map(),
|
||||||
},
|
},
|
||||||
mutable: {},
|
mutable: { globalMutable },
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = await runPromiseThrowChain(() =>
|
const newState = await runPromiseThrowChain(() =>
|
||||||
|
|
|
@ -38,11 +38,15 @@ export interface Mutable {
|
||||||
prefetchCache?: AppRouterState['prefetchCache']
|
prefetchCache?: AppRouterState['prefetchCache']
|
||||||
hashFragment?: string
|
hashFragment?: string
|
||||||
shouldScroll?: boolean
|
shouldScroll?: boolean
|
||||||
|
globalMutable: {
|
||||||
|
pendingNavigatePath?: string
|
||||||
|
pendingMpaPath?: string
|
||||||
|
refresh: () => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerActionMutable extends Mutable {
|
export interface ServerActionMutable extends Mutable {
|
||||||
inFlightServerAction?: Promise<any> | null
|
inFlightServerAction?: Promise<any> | null
|
||||||
globalMutable: { pendingNavigatePath?: string; refresh: () => void }
|
|
||||||
actionResultResolved?: boolean
|
actionResultResolved?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
38
test/e2e/app-dir/navigation/app/mpa-nav-test/page.js
Normal file
38
test/e2e/app-dir/navigation/app/mpa-nav-test/page.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
'use client'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const prefetchRef = useRef()
|
||||||
|
const slowPageRef = useRef()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function triggerPrefetch() {
|
||||||
|
const event = new MouseEvent('mouseover', {
|
||||||
|
view: window,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
prefetchRef.current.dispatchEvent(event)
|
||||||
|
console.log('dispatched')
|
||||||
|
}
|
||||||
|
|
||||||
|
slowPageRef.current.click()
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
triggerPrefetch()
|
||||||
|
}, 1000)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link id="link-to-slow-page" href="/slow-page" ref={slowPageRef}>
|
||||||
|
To /slow-page
|
||||||
|
</Link>
|
||||||
|
<Link id="prefetch-link" href="/hash" ref={prefetchRef}>
|
||||||
|
Prefetch link
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { createNextDescribe } from 'e2e-utils'
|
import { createNextDescribe } from 'e2e-utils'
|
||||||
import { check } from 'next-test-utils'
|
import { check, waitFor } from 'next-test-utils'
|
||||||
import type { Request } from 'playwright-chromium'
|
import type { Request } from 'playwright-chromium'
|
||||||
|
|
||||||
createNextDescribe(
|
createNextDescribe(
|
||||||
|
@ -497,6 +497,33 @@ createNextDescribe(
|
||||||
.waitForElementByCss('#link-to-app')
|
.waitForElementByCss('#link-to-app')
|
||||||
expect(await browser.url()).toBe(next.url + '/some')
|
expect(await browser.url()).toBe(next.url + '/some')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!isNextDev) {
|
||||||
|
// this test is pretty hard to test in playwright, so most of the heavy lifting is in the page component itself
|
||||||
|
// it triggers a hover on a link to initiate a prefetch request every second, and so we check that
|
||||||
|
// it doesn't repeatedly initiate the mpa navigation request
|
||||||
|
it('should not continously initiate a mpa navigation to the same URL when router state changes', async () => {
|
||||||
|
let requestCount = 0
|
||||||
|
const browser = await next.browser('/mpa-nav-test', {
|
||||||
|
beforePageLoad(page) {
|
||||||
|
page.on('request', (request) => {
|
||||||
|
const url = new URL(request.url())
|
||||||
|
// skip rsc prefetches
|
||||||
|
if (url.pathname === '/slow-page' && !url.search) {
|
||||||
|
requestCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await browser.waitForElementByCss('#link-to-slow-page')
|
||||||
|
|
||||||
|
// wait a few seconds since prefetches are triggered in 1s intervals in the page component
|
||||||
|
await waitFor(5000)
|
||||||
|
|
||||||
|
expect(requestCount).toBe(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('nested navigation', () => {
|
describe('nested navigation', () => {
|
||||||
|
@ -562,7 +589,7 @@ createNextDescribe(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should emit refresh meta tag (peramnent) for redirect page when streaming', async () => {
|
it('should emit refresh meta tag (permanent) for redirect page when streaming', async () => {
|
||||||
const html = await next.render('/redirect/suspense-2')
|
const html = await next.render('/redirect/suspense-2')
|
||||||
expect(html).toContain(
|
expect(html).toContain(
|
||||||
'<meta http-equiv="refresh" content="0;url=/redirect/result"/>'
|
'<meta http-equiv="refresh" content="0;url=/redirect/result"/>'
|
||||||
|
|
13
test/e2e/app-dir/navigation/pages/slow-page.js
Normal file
13
test/e2e/app-dir/navigation/pages/slow-page.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export default function Page() {
|
||||||
|
return 'Hello from slow page'
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps({ resolvedUrl }) {
|
||||||
|
if (!resolvedUrl.includes('?_rsc')) {
|
||||||
|
// only stall on the navigation, not prefetch
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100000))
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue