Skip to content

Commit e368cc5

Browse files
fix: old data kept on new route during transition (#730)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
1 parent eb4966e commit e368cc5

File tree

4 files changed

+89
-33
lines changed

4 files changed

+89
-33
lines changed

packages/tuono-router/src/components/RouteMatch.spec.tsx

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { cleanup, render, screen } from '@testing-library/react'
44
import { Route } from '../route'
55
import type { RouteComponent, RouteProps } from '../types'
66
import { useServerPayloadData } from '../hooks/useServerPayloadData'
7+
import { useRouterContext } from '../components/RouterContext'
78

89
import { RouteMatch } from './RouteMatch'
910

@@ -47,16 +48,21 @@ vi.mock('../hooks/useServerPayloadData', () => ({
4748
useServerPayloadData: vi.fn(),
4849
}))
4950

51+
vi.mock('../components/RouterContext', () => ({
52+
useRouterContext: vi.fn(),
53+
}))
54+
5055
const useServerPayloadDataMock = vi.mocked(useServerPayloadData)
56+
const useRouterContextMock = vi.mocked(useRouterContext)
5157

5258
describe('<RouteMatch />', () => {
5359
afterEach(cleanup)
5460

5561
it('should correctly render nested routes', () => {
56-
useServerPayloadDataMock.mockReturnValue({
57-
data: { some: 'data' },
58-
isLoading: false,
59-
})
62+
useServerPayloadDataMock.mockReturnValue({ data: { some: 'data' } })
63+
64+
// @ts-expect-error only isTransitioning is used by RouteMatch
65+
useRouterContextMock.mockReturnValue({ isTransitioning: false })
6066

6167
render(<RouteMatch route={route} serverInitialData={{}} />)
6268

@@ -81,13 +87,15 @@ describe('<RouteMatch />', () => {
8187
)
8288
})
8389

84-
it('should return null data when while loading', () => {
85-
useServerPayloadDataMock.mockReturnValue({
86-
data: { some: 'data' },
87-
isLoading: true,
88-
})
90+
it('should correctly handle loading transition', () => {
91+
useServerPayloadDataMock.mockReturnValue({ data: { some: 'data' } })
8992

90-
render(<RouteMatch route={route} serverInitialData={{}} />)
93+
// @ts-expect-error only isTransitioning is used by RouteMatch
94+
useRouterContextMock.mockReturnValue({ isTransitioning: true })
95+
96+
const { rerender } = render(
97+
<RouteMatch route={route} serverInitialData={{}} />,
98+
)
9199

92100
expect(screen.getByTestId('root')).toMatchInlineSnapshot(
93101
`
@@ -106,5 +114,30 @@ describe('<RouteMatch />', () => {
106114
</div>
107115
`,
108116
)
117+
118+
// @ts-expect-error only isTransitioning is used by RouteMatch
119+
useRouterContextMock.mockReturnValue({ isTransitioning: false })
120+
121+
rerender(<RouteMatch route={route} serverInitialData={{}} />)
122+
123+
expect(screen.getByTestId('root')).toMatchInlineSnapshot(
124+
`
125+
<div
126+
data-testid="root"
127+
>
128+
root route
129+
<div
130+
data-testid="parent"
131+
>
132+
parent route
133+
<div
134+
data-testid="current"
135+
>
136+
{"some":"data"}
137+
</div>
138+
</div>
139+
</div>
140+
`,
141+
)
109142
})
110143
})

packages/tuono-router/src/components/RouteMatch.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import type { JSX } from 'react'
22
import { memo, Suspense, useMemo } from 'react'
33

44
import type { Route } from '../route'
5+
56
import { useServerPayloadData } from '../hooks/useServerPayloadData'
67

8+
import { useRouterContext } from './RouterContext'
9+
710
interface RouteMatchProps<TServerPayloadData = unknown> {
811
route: Route
912
// User defined server side props
@@ -19,21 +22,22 @@ export const RouteMatch = ({
1922
route,
2023
serverInitialData,
2124
}: RouteMatchProps): JSX.Element => {
22-
const { data, isLoading } = useServerPayloadData(route, serverInitialData)
25+
const { data } = useServerPayloadData(route, serverInitialData)
26+
const { isTransitioning } = useRouterContext()
2327

2428
// eslint-disable-next-line react-hooks/exhaustive-deps
2529
const routes = useMemo(() => loadParentComponents(route), [route.id])
2630

27-
const routeData = isLoading ? null : data
31+
const routeData = isTransitioning ? null : data
2832

2933
return (
3034
<TraverseRootComponents
3135
routes={routes}
3236
data={routeData}
33-
isLoading={isLoading}
37+
isLoading={isTransitioning}
3438
>
3539
<Suspense>
36-
<route.component data={routeData} isLoading={isLoading} />
40+
<route.component data={routeData} isLoading={isTransitioning} />
3741
</Suspense>
3842
</TraverseRootComponents>
3943
)

packages/tuono-router/src/components/RouterContext.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { createContext, useState, useEffect, useContext, useMemo } from 'react'
1+
import {
2+
createContext,
3+
useState,
4+
useEffect,
5+
useContext,
6+
useMemo,
7+
useCallback,
8+
} from 'react'
29
import type { ReactNode } from 'react'
310

411
import type { Router } from '../router'
@@ -17,7 +24,9 @@ export interface ParsedLocation {
1724
interface RouterContextValue {
1825
router: Router
1926
location: ParsedLocation
27+
isTransitioning: boolean
2028
updateLocation: (loc: ParsedLocation) => void
29+
stopTransitioning: () => void
2130
}
2231

2332
const RouterContext = createContext({} as RouterContextValue)
@@ -64,6 +73,9 @@ export function RouterContextProvider({
6473
const [location, setLocation] = useState<ParsedLocation>(() =>
6574
getInitialLocation(serverInitialLocation),
6675
)
76+
// Global state to track whether a page transition is in progress.
77+
// Set to `false` once the page is fully loaded, including server-side data.
78+
const [isTransitioning, setIsTransitioning] = useState<boolean>(false)
6779

6880
/**
6981
* Listen browser navigation events
@@ -91,13 +103,24 @@ export function RouterContextProvider({
91103
}
92104
}, [])
93105

106+
const updateLocation = useCallback((newLocation: ParsedLocation): void => {
107+
setIsTransitioning(true)
108+
setLocation(newLocation)
109+
}, [])
110+
111+
const stopTransitioning = useCallback((): void => {
112+
setIsTransitioning(false)
113+
}, [])
114+
94115
const contextValue: RouterContextValue = useMemo(
95116
() => ({
96117
router,
97118
location,
98-
updateLocation: setLocation,
119+
isTransitioning,
120+
updateLocation,
121+
stopTransitioning,
99122
}),
100-
[location, router],
123+
[location, router, isTransitioning, updateLocation, stopTransitioning],
101124
)
102125

103126
return (

packages/tuono-router/src/hooks/useServerPayloadData.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import { fromUrlToParsedLocation } from '../utils/from-url-to-parsed-location'
55

66
import { useRouterContext } from '../components/RouterContext'
77

8-
const isServer = typeof document === 'undefined'
9-
108
interface TuonoApi {
119
data?: unknown
1210
info: {
@@ -22,7 +20,6 @@ const fetchClientSideData = async (): Promise<TuonoApi> => {
2220

2321
interface UseServerPayloadDataResult<TData> {
2422
data: TData
25-
isLoading: boolean
2623
}
2724

2825
/*
@@ -37,15 +34,7 @@ export function useServerPayloadData<TServerPayloadData>(
3734
serverInitialData: TServerPayloadData,
3835
): UseServerPayloadDataResult<TServerPayloadData> {
3936
const isFirstRendering = useRef<boolean>(true)
40-
const { location, updateLocation } = useRouterContext()
41-
const [isLoading, setIsLoading] = useState<boolean>(
42-
// Force loading if has handler
43-
!!route.options.hasHandler &&
44-
// Avoid loading on the server
45-
!isServer &&
46-
// Avoid loading if first rendering
47-
!isFirstRendering.current,
48-
)
37+
const { location, updateLocation, stopTransitioning } = useRouterContext()
4938

5039
const [data, setData] = useState<TServerPayloadData | undefined>(
5140
serverInitialData,
@@ -56,14 +45,14 @@ export function useServerPayloadData<TServerPayloadData>(
5645
// props are already bundled by the SSR
5746
if (isFirstRendering.current) {
5847
isFirstRendering.current = false
48+
stopTransitioning()
5949
return
6050
}
6151
// After client side routing load again the remote data
6252
if (route.options.hasHandler) {
6353
// The error management is already handled inside the IIFE
6454
// eslint-disable-next-line @typescript-eslint/no-floating-promises
6555
;(async (): Promise<void> => {
66-
setIsLoading(true)
6756
try {
6857
const response = await fetchClientSideData()
6958
if (response.info.redirect_destination) {
@@ -84,16 +73,23 @@ export function useServerPayloadData<TServerPayloadData>(
8473
} catch (error) {
8574
throw Error('Failed loading Server Side Data', { cause: error })
8675
} finally {
87-
setIsLoading(false)
76+
stopTransitioning()
8877
}
8978
})()
79+
} else {
80+
stopTransitioning()
9081
}
9182

9283
// Clean up the data when changing route
9384
return (): void => {
9485
setData(undefined)
9586
}
96-
}, [location.pathname, route.options.hasHandler, updateLocation])
87+
}, [
88+
location.pathname,
89+
route.options.hasHandler,
90+
updateLocation,
91+
stopTransitioning,
92+
])
9793

98-
return { isLoading, data: data as TServerPayloadData }
94+
return { data: data as TServerPayloadData }
9995
}

0 commit comments

Comments
 (0)