fix(next): clear bfcache on forward/back (#13913)
Fixes #12914. Using the forward/back browser navigation shows stale data from the previous visit. For example: 1. Visit the list view, imagine a document with a title of "123" 2. Navigate to that document, update the title to "456" 3. Press the "back" button in the browser 4. Page incorrectly shows "123" 5. Press the "forward" button in the browser 6. Page incorrectly shows "123" This is because Next.js caches those pages in memory in their [Client-side Router Cache](https://nextjs.org/docs/app/guides/caching#client-side-router-cache). This enables instant loads during forward and back navigation by restoring the previously cached response instead of making a new request—which also happens to be our exact problem. This bfcache-like behavior is not able to be opted out of, even if the page requires authentication, etc. The [hopefully temporary] fix is to force the router to make a new request on forward/back navigation. We can do this by listening to the popstate event and calling `router.refresh()`. This does create a flash of stale content, however, because the refresh takes place _after_ the cache was restored. While not wonderful, this is targeted to specifically the forward/back events, and it's technically not duplicative as the restored cache never made a request in the first place. Without native support, I'm not sure how else we'd achieve this, as there's not way to invalidate the list view from a deeply nested document drawer, for example. Before: https://github.com/user-attachments/assets/751b33b2-1926-47d2-acba-b1d47df06a6d After: https://github.com/user-attachments/assets/fe71938a-5c64-4756-a2c7-45dced4fcaaa --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211454879207837
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname, useRouter } from 'next/navigation.js'
|
||||
import React, { createContext, use, useCallback, useEffect } from 'react'
|
||||
import React, { createContext, use, useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
|
||||
|
||||
export type RouteCacheContext = {
|
||||
cachingEnabled: boolean
|
||||
@@ -20,15 +22,41 @@ export const RouteCache: React.FC<{ cachingEnabled?: boolean; children: React.Re
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* Next.js caches pages in memory in their {@link https://nextjs.org/docs/app/guides/caching#client-side-router-cache Client-side Router Cache},
|
||||
* causing forward/back browser navigation to show stale data from a previous visit.
|
||||
* The problem is this bfcache-like behavior has no opt-out, even if the page is dynamic, requires authentication, etc.
|
||||
* The workaround is to force a refresh when navigating via the browser controls.
|
||||
* This should be removed if/when Next.js supports this natively.
|
||||
*/
|
||||
const clearAfterPathnameChange = useRef(false)
|
||||
|
||||
const clearRouteCache = useCallback(() => {
|
||||
router.refresh()
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
if (cachingEnabled) {
|
||||
const handlePopState = () => {
|
||||
clearAfterPathnameChange.current = true
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
const handlePathnameChange = useEffectEvent((pathname: string) => {
|
||||
if (cachingEnabled || clearAfterPathnameChange.current) {
|
||||
clearAfterPathnameChange.current = false
|
||||
clearRouteCache()
|
||||
}
|
||||
}, [pathname, clearRouteCache, cachingEnabled])
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
handlePathnameChange(pathname)
|
||||
}, [pathname])
|
||||
|
||||
return <Context value={{ cachingEnabled, clearRouteCache }}>{children}</Context>
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import { openNav } from 'helpers/e2e/toggleNav.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
|
||||
@@ -377,6 +378,39 @@ describe('General', () => {
|
||||
// Should redirect to dashboard
|
||||
await expect.poll(() => page.url()).toBe(`${serverURL}/admin`)
|
||||
})
|
||||
|
||||
/**
|
||||
* This test is skipped because `page.goBack()` and `page.goForward()` do not trigger navigation in the Next.js app.
|
||||
* I also tried rendering buttons that call `router.back()` and click those instead, but that also does not work.
|
||||
*/
|
||||
test.skip("should clear the router's bfcache when navigating via the forward/back browser controls", async () => {
|
||||
const { id } = await createPost({
|
||||
title: 'Post to test bfcache',
|
||||
})
|
||||
|
||||
// check for it in the list view first
|
||||
await page.goto(postsUrl.list)
|
||||
const cell = page.locator('.table td').filter({ hasText: 'Post to test bfcache' })
|
||||
await page.locator('.table a').filter({ hasText: id }).click()
|
||||
|
||||
await page.waitForURL(`${postsUrl.edit(id)}`)
|
||||
const titleField = page.locator('#field-title')
|
||||
await expect(titleField).toHaveValue('Post to test bfcache')
|
||||
|
||||
// change the title to something else
|
||||
await titleField.fill('Post to test bfcache - updated')
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
// now use the browser controls to go back to the list
|
||||
await page.goBack()
|
||||
await page.waitForURL(postsUrl.list)
|
||||
await expect(cell).toBeVisible()
|
||||
|
||||
// and then forward to the edit page again
|
||||
await page.goForward()
|
||||
await page.waitForURL(`${postsUrl.edit(id)}`)
|
||||
await expect(titleField).toHaveValue('Post to test bfcache - updated')
|
||||
})
|
||||
})
|
||||
|
||||
describe('navigation', () => {
|
||||
|
||||
Reference in New Issue
Block a user