File tree Expand file tree Collapse file tree 8 files changed +115
-12
lines changed
packages/next/src/client/components Expand file tree Collapse file tree 8 files changed +115
-12
lines changed Original file line number Diff line number Diff line change @@ -48,6 +48,13 @@ export class RedirectErrorBoundary extends React.Component<
4848 if ( isRedirectError ( error ) ) {
4949 const url = getURLFromRedirectError ( error )
5050 const redirectType = getRedirectTypeFromError ( error )
51+ if ( 'handled' in error ) {
52+ // The redirect was already handled. We'll still catch the redirect error
53+ // so that we can remount the subtree, but we don't actually need to trigger the
54+ // router.push.
55+ return { redirect : null , redirectType : null }
56+ }
57+
5158 return { redirect : url , redirectType }
5259 }
5360 // Re-throw if error is not for redirect
Original file line number Diff line number Diff line change @@ -415,19 +415,19 @@ export function serverActionReducer(
415415 // the component that called the action as the error boundary will remount the tree.
416416 // The status code doesn't matter here as the action handler will have already sent
417417 // a response with the correct status code.
418- reject (
419- getRedirectError (
420- hasBasePath ( redirectHref )
421- ? removeBasePath ( redirectHref )
422- : redirectHref ,
423- redirectType || RedirectType . push
424- )
418+ const redirectError = getRedirectError (
419+ hasBasePath ( redirectHref )
420+ ? removeBasePath ( redirectHref )
421+ : redirectHref ,
422+ redirectType || RedirectType . push
425423 )
426-
427- // TODO: Investigate why this is needed with Activity.
428- if ( process . env . __NEXT_CACHE_COMPONENTS ) {
429- return state
430- }
424+ // We mark the error as handled because we don't want the redirect to be tried later by
425+ // the RedirectBoundary, in case the user goes back and `Activity` triggers the redirect
426+ // again, as it's run within an effect.
427+ // We don't actually need the RedirectBoundary to do a router.push because we already
428+ // have all the necessary RSC data to render the new page within a single roundtrip.
429+ ; ( redirectError as any ) . handled = true
430+ reject ( redirectError )
431431 } else {
432432 resolve ( actionResult )
433433 }
Original file line number Diff line number Diff line change 1+ 'use server'
2+
3+ import { cookies } from 'next/headers'
4+ import { refresh } from 'next/cache'
5+ import { redirect } from 'next/navigation'
6+
7+ export async function addTodoEntry ( entry : string ) {
8+ // simulate some latency
9+ await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) )
10+
11+ const cookieStore = await cookies ( )
12+ const existing = cookieStore . get ( 'todo-entries' ) ?. value || '[]'
13+ const entries = JSON . parse ( existing )
14+ entries . push ( entry )
15+ cookieStore . set ( 'todo-entries' , JSON . stringify ( entries ) )
16+ }
17+
18+ export async function getTodoEntries ( ) {
19+ const cookieStore = await cookies ( )
20+ const existing = cookieStore . get ( 'todo-entries' ) ?. value || '[]'
21+ return JSON . parse ( existing ) as string [ ]
22+ }
23+
24+ export async function addEntryAndRefresh ( formData : FormData ) {
25+ const entry = formData . get ( 'entry' ) as string
26+
27+ await addTodoEntry ( entry )
28+
29+ refresh ( )
30+ redirect ( '/redirect-and-refresh/foo' )
31+ }
Original file line number Diff line number Diff line change 1+ export default function Foo ( ) {
2+ return < div id = "foo-page" > Hello from Foo</ div >
3+ }
Original file line number Diff line number Diff line change 1+ import { getTodoEntries } from './actions'
2+
3+ export default async function Layout ( { children } ) {
4+ const entries = await getTodoEntries ( )
5+
6+ console . log ( { entries } )
7+ return (
8+ < div >
9+ < div id = "todo-entries" >
10+ { entries . length > 0 ? (
11+ entries . map ( ( phrase , index ) => < div key = { index } > { phrase } </ div > )
12+ ) : (
13+ < div id = "no-entries" > No entries</ div >
14+ ) }
15+ </ div >
16+ < div > { children } </ div >
17+ </ div >
18+ )
19+ }
Original file line number Diff line number Diff line change 1+ import { addEntryAndRefresh } from './actions'
2+
3+ export default function Page ( ) {
4+ return (
5+ < >
6+ < form action = { addEntryAndRefresh } >
7+ < label htmlFor = "todo-input" > Entry</ label >
8+ < input
9+ id = "todo-input"
10+ type = "text"
11+ name = "entry"
12+ placeholder = "Enter a new entry"
13+ />
14+ < button id = "add-button" type = "submit" >
15+ Add New Todo Entry
16+ </ button >
17+ </ form >
18+ </ >
19+ )
20+ }
Original file line number Diff line number Diff line change 1+ module . exports = {
2+ cacheComponents : true ,
3+ }
Original file line number Diff line number Diff line change @@ -80,4 +80,24 @@ describe('app-dir refresh', () => {
8080 } )
8181 }
8282 } )
83+
84+ it ( 'should let you read your write after a redirect and refresh' , async ( ) => {
85+ const browser = await next . browser ( '/redirect-and-refresh' )
86+
87+ const todoEntries = await browser . elementById ( 'todo-entries' ) . text ( )
88+ expect ( todoEntries ) . toBe ( 'No entries' )
89+
90+ const todoInput = await browser . elementById ( 'todo-input' )
91+ await todoInput . fill ( 'foo' )
92+
93+ await browser . elementById ( 'add-button' ) . click ( )
94+
95+ await retry ( async ( ) => {
96+ const newTodoEntries = await browser . elementById ( 'todo-entries' ) . text ( )
97+ expect ( newTodoEntries ) . toContain ( 'foo' )
98+ } )
99+
100+ expect ( await browser . hasElementByCssSelector ( '#foo-page' ) ) . toBe ( true )
101+ expect ( await browser . url ( ) ) . toContain ( '/redirect-and-refresh/foo' )
102+ } )
83103} )
You can’t perform that action at this time.
0 commit comments