diff --git a/packages/tuono-router/src/components/RouteMatch.spec.tsx b/packages/tuono-router/src/components/RouteMatch.spec.tsx
index a2236f19..6dba91f3 100644
--- a/packages/tuono-router/src/components/RouteMatch.spec.tsx
+++ b/packages/tuono-router/src/components/RouteMatch.spec.tsx
@@ -4,6 +4,7 @@ import { cleanup, render, screen } from '@testing-library/react'
import { Route } from '../route'
import type { RouteComponent, RouteProps } from '../types'
import { useServerPayloadData } from '../hooks/useServerPayloadData'
+import { useRouterContext } from '../components/RouterContext'
import { RouteMatch } from './RouteMatch'
@@ -47,16 +48,21 @@ vi.mock('../hooks/useServerPayloadData', () => ({
useServerPayloadData: vi.fn(),
}))
+vi.mock('../components/RouterContext', () => ({
+ useRouterContext: vi.fn(),
+}))
+
const useServerPayloadDataMock = vi.mocked(useServerPayloadData)
+const useRouterContextMock = vi.mocked(useRouterContext)
describe('', () => {
afterEach(cleanup)
it('should correctly render nested routes', () => {
- useServerPayloadDataMock.mockReturnValue({
- data: { some: 'data' },
- isLoading: false,
- })
+ useServerPayloadDataMock.mockReturnValue({ data: { some: 'data' } })
+
+ // @ts-expect-error only isTransitioning is used by RouteMatch
+ useRouterContextMock.mockReturnValue({ isTransitioning: false })
render()
@@ -81,13 +87,15 @@ describe('', () => {
)
})
- it('should return null data when while loading', () => {
- useServerPayloadDataMock.mockReturnValue({
- data: { some: 'data' },
- isLoading: true,
- })
+ it('should correctly handle loading transition', () => {
+ useServerPayloadDataMock.mockReturnValue({ data: { some: 'data' } })
- render()
+ // @ts-expect-error only isTransitioning is used by RouteMatch
+ useRouterContextMock.mockReturnValue({ isTransitioning: true })
+
+ const { rerender } = render(
+ ,
+ )
expect(screen.getByTestId('root')).toMatchInlineSnapshot(
`
@@ -106,5 +114,30 @@ describe('', () => {
`,
)
+
+ // @ts-expect-error only isTransitioning is used by RouteMatch
+ useRouterContextMock.mockReturnValue({ isTransitioning: false })
+
+ rerender()
+
+ expect(screen.getByTestId('root')).toMatchInlineSnapshot(
+ `
+
+ root route
+
+ parent route
+
+ {"some":"data"}
+
+
+
+ `,
+ )
})
})
diff --git a/packages/tuono-router/src/components/RouteMatch.tsx b/packages/tuono-router/src/components/RouteMatch.tsx
index 40519c68..1db74771 100644
--- a/packages/tuono-router/src/components/RouteMatch.tsx
+++ b/packages/tuono-router/src/components/RouteMatch.tsx
@@ -2,8 +2,11 @@ import type { JSX } from 'react'
import { memo, Suspense, useMemo } from 'react'
import type { Route } from '../route'
+
import { useServerPayloadData } from '../hooks/useServerPayloadData'
+import { useRouterContext } from './RouterContext'
+
interface RouteMatchProps {
route: Route
// User defined server side props
@@ -19,21 +22,22 @@ export const RouteMatch = ({
route,
serverInitialData,
}: RouteMatchProps): JSX.Element => {
- const { data, isLoading } = useServerPayloadData(route, serverInitialData)
+ const { data } = useServerPayloadData(route, serverInitialData)
+ const { isTransitioning } = useRouterContext()
// eslint-disable-next-line react-hooks/exhaustive-deps
const routes = useMemo(() => loadParentComponents(route), [route.id])
- const routeData = isLoading ? null : data
+ const routeData = isTransitioning ? null : data
return (
-
+
)
diff --git a/packages/tuono-router/src/components/RouterContext.tsx b/packages/tuono-router/src/components/RouterContext.tsx
index 101c10df..0bf9df88 100644
--- a/packages/tuono-router/src/components/RouterContext.tsx
+++ b/packages/tuono-router/src/components/RouterContext.tsx
@@ -1,4 +1,11 @@
-import { createContext, useState, useEffect, useContext, useMemo } from 'react'
+import {
+ createContext,
+ useState,
+ useEffect,
+ useContext,
+ useMemo,
+ useCallback,
+} from 'react'
import type { ReactNode } from 'react'
import type { Router } from '../router'
@@ -17,7 +24,9 @@ export interface ParsedLocation {
interface RouterContextValue {
router: Router
location: ParsedLocation
+ isTransitioning: boolean
updateLocation: (loc: ParsedLocation) => void
+ stopTransitioning: () => void
}
const RouterContext = createContext({} as RouterContextValue)
@@ -64,6 +73,9 @@ export function RouterContextProvider({
const [location, setLocation] = useState(() =>
getInitialLocation(serverInitialLocation),
)
+ // Global state to track whether a page transition is in progress.
+ // Set to `false` once the page is fully loaded, including server-side data.
+ const [isTransitioning, setIsTransitioning] = useState(false)
/**
* Listen browser navigation events
@@ -91,13 +103,24 @@ export function RouterContextProvider({
}
}, [])
+ const updateLocation = useCallback((newLocation: ParsedLocation): void => {
+ setIsTransitioning(true)
+ setLocation(newLocation)
+ }, [])
+
+ const stopTransitioning = useCallback((): void => {
+ setIsTransitioning(false)
+ }, [])
+
const contextValue: RouterContextValue = useMemo(
() => ({
router,
location,
- updateLocation: setLocation,
+ isTransitioning,
+ updateLocation,
+ stopTransitioning,
}),
- [location, router],
+ [location, router, isTransitioning, updateLocation, stopTransitioning],
)
return (
diff --git a/packages/tuono-router/src/hooks/useServerPayloadData.ts b/packages/tuono-router/src/hooks/useServerPayloadData.ts
index 74aa50e4..28684b14 100644
--- a/packages/tuono-router/src/hooks/useServerPayloadData.ts
+++ b/packages/tuono-router/src/hooks/useServerPayloadData.ts
@@ -5,8 +5,6 @@ import { fromUrlToParsedLocation } from '../utils/from-url-to-parsed-location'
import { useRouterContext } from '../components/RouterContext'
-const isServer = typeof document === 'undefined'
-
interface TuonoApi {
data?: unknown
info: {
@@ -22,7 +20,6 @@ const fetchClientSideData = async (): Promise => {
interface UseServerPayloadDataResult {
data: TData
- isLoading: boolean
}
/*
@@ -37,15 +34,7 @@ export function useServerPayloadData(
serverInitialData: TServerPayloadData,
): UseServerPayloadDataResult {
const isFirstRendering = useRef(true)
- const { location, updateLocation } = useRouterContext()
- const [isLoading, setIsLoading] = useState(
- // Force loading if has handler
- !!route.options.hasHandler &&
- // Avoid loading on the server
- !isServer &&
- // Avoid loading if first rendering
- !isFirstRendering.current,
- )
+ const { location, updateLocation, stopTransitioning } = useRouterContext()
const [data, setData] = useState(
serverInitialData,
@@ -56,6 +45,7 @@ export function useServerPayloadData(
// props are already bundled by the SSR
if (isFirstRendering.current) {
isFirstRendering.current = false
+ stopTransitioning()
return
}
// After client side routing load again the remote data
@@ -63,7 +53,6 @@ export function useServerPayloadData(
// The error management is already handled inside the IIFE
// eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async (): Promise => {
- setIsLoading(true)
try {
const response = await fetchClientSideData()
if (response.info.redirect_destination) {
@@ -84,16 +73,23 @@ export function useServerPayloadData(
} catch (error) {
throw Error('Failed loading Server Side Data', { cause: error })
} finally {
- setIsLoading(false)
+ stopTransitioning()
}
})()
+ } else {
+ stopTransitioning()
}
// Clean up the data when changing route
return (): void => {
setData(undefined)
}
- }, [location.pathname, route.options.hasHandler, updateLocation])
+ }, [
+ location.pathname,
+ route.options.hasHandler,
+ updateLocation,
+ stopTransitioning,
+ ])
- return { isLoading, data: data as TServerPayloadData }
+ return { data: data as TServerPayloadData }
}