Skip to content

Commit f868ed9

Browse files
authored
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
1 parent 7bbd07c commit f868ed9

2 files changed

Lines changed: 65 additions & 3 deletions

File tree

packages/ui/src/providers/RouteCache/index.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
'use client'
22

33
import { usePathname, useRouter } from 'next/navigation.js'
4-
import React, { createContext, use, useCallback, useEffect } from 'react'
4+
import React, { createContext, use, useCallback, useEffect, useRef } from 'react'
5+
6+
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
57

68
export type RouteCacheContext = {
79
cachingEnabled: boolean
@@ -20,15 +22,41 @@ export const RouteCache: React.FC<{ cachingEnabled?: boolean; children: React.Re
2022
const pathname = usePathname()
2123
const router = useRouter()
2224

25+
/**
26+
* Next.js caches pages in memory in their {@link https://nextjs.org/docs/app/guides/caching#client-side-router-cache Client-side Router Cache},
27+
* causing forward/back browser navigation to show stale data from a previous visit.
28+
* The problem is this bfcache-like behavior has no opt-out, even if the page is dynamic, requires authentication, etc.
29+
* The workaround is to force a refresh when navigating via the browser controls.
30+
* This should be removed if/when Next.js supports this natively.
31+
*/
32+
const clearAfterPathnameChange = useRef(false)
33+
2334
const clearRouteCache = useCallback(() => {
2435
router.refresh()
2536
}, [router])
2637

2738
useEffect(() => {
28-
if (cachingEnabled) {
39+
const handlePopState = () => {
40+
clearAfterPathnameChange.current = true
41+
}
42+
43+
window.addEventListener('popstate', handlePopState)
44+
45+
return () => {
46+
window.removeEventListener('popstate', handlePopState)
47+
}
48+
}, [router])
49+
50+
const handlePathnameChange = useEffectEvent((pathname: string) => {
51+
if (cachingEnabled || clearAfterPathnameChange.current) {
52+
clearAfterPathnameChange.current = false
2953
clearRouteCache()
3054
}
31-
}, [pathname, clearRouteCache, cachingEnabled])
55+
})
56+
57+
useEffect(() => {
58+
handlePathnameChange(pathname)
59+
}, [pathname])
3260

3361
return <Context value={{ cachingEnabled, clearRouteCache }}>{children}</Context>
3462
}

test/admin/e2e/general/e2e.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
5353
import { openDocControls } from 'helpers/e2e/openDocControls.js'
5454
import { openNav } from 'helpers/e2e/toggleNav.js'
5555
import path from 'path'
56+
import { wait } from 'payload/shared'
5657
import { fileURLToPath } from 'url'
5758

5859
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
@@ -377,6 +378,39 @@ describe('General', () => {
377378
// Should redirect to dashboard
378379
await expect.poll(() => page.url()).toBe(`${serverURL}/admin`)
379380
})
381+
382+
/**
383+
* This test is skipped because `page.goBack()` and `page.goForward()` do not trigger navigation in the Next.js app.
384+
* I also tried rendering buttons that call `router.back()` and click those instead, but that also does not work.
385+
*/
386+
test.skip("should clear the router's bfcache when navigating via the forward/back browser controls", async () => {
387+
const { id } = await createPost({
388+
title: 'Post to test bfcache',
389+
})
390+
391+
// check for it in the list view first
392+
await page.goto(postsUrl.list)
393+
const cell = page.locator('.table td').filter({ hasText: 'Post to test bfcache' })
394+
await page.locator('.table a').filter({ hasText: id }).click()
395+
396+
await page.waitForURL(`${postsUrl.edit(id)}`)
397+
const titleField = page.locator('#field-title')
398+
await expect(titleField).toHaveValue('Post to test bfcache')
399+
400+
// change the title to something else
401+
await titleField.fill('Post to test bfcache - updated')
402+
await saveDocAndAssert(page)
403+
404+
// now use the browser controls to go back to the list
405+
await page.goBack()
406+
await page.waitForURL(postsUrl.list)
407+
await expect(cell).toBeVisible()
408+
409+
// and then forward to the edit page again
410+
await page.goForward()
411+
await page.waitForURL(`${postsUrl.edit(id)}`)
412+
await expect(titleField).toHaveValue('Post to test bfcache - updated')
413+
})
380414
})
381415

382416
describe('navigation', () => {

0 commit comments

Comments
 (0)