Skip to content

Commit eaf6221

Browse files
authored
feat(rsc): Initial support for RSA rerender (#11406)
1 parent 2cd4521 commit eaf6221

File tree

3 files changed

+104
-39
lines changed

3 files changed

+104
-39
lines changed

.changesets/11406.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- feat(rsc): Initial support for RSA rerender (#11406) by @Tobbe
2+
3+
This PR makes it so that the entire page is re-rendered when a React Server Action returns.
4+
Previously when calling an RSA you'd only get the result of the action back.
5+
Now, when calling an RSA you'll still get the result back, and in addition to that the page will update.
6+
What this means is that if you for example update a counter on the server that a server component is displaying that counter will now immediately update.
7+
Also, if some data has been updated by something external to the app the new data will be displayed (like if someone used an external CMS to update some .md file you're rendering)

packages/router/src/rsc/ClientRouter.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ const LocationAwareRouter = ({
8787
}
8888

8989
const rscProps = { location: { pathname, search } }
90+
// TODO (RSC): I think that moving between private and public routes
91+
// re-initializes RscFetcher. I wonder if there's an optimization to be made
92+
// here. Maybe we can lift RscFetcher up so we can keep the same instance
93+
// around and reuse it everywhere
9094
return <RscFetcher rscId="__rwjs__Routes" rscProps={rscProps} />
9195
}
9296

+93-39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type React from 'react'
2-
import { useState, useEffect } from 'react'
2+
import { use, useState, useEffect } from 'react'
33

44
import type { Options } from 'react-server-dom-webpack/client'
55
import { createFromFetch, encodeReply } from 'react-server-dom-webpack/client'
@@ -17,27 +17,48 @@ export interface RscProps extends Record<string, unknown> {
1717
}
1818
}
1919

20-
export function rscFetch(
21-
rscId: string,
22-
serializedProps: string,
23-
// setComponent?: (component: Thenable<React.ReactElement>) => void,
20+
let updateCurrentRscCacheKey = (key: string) => {
21+
console.error('updateCurrentRscCacheKey called before it was set')
22+
console.error('updateCurrentRscCacheKey key', key)
23+
}
24+
25+
function onStreamFinished(
26+
fetchPromise: ReturnType<typeof fetch>,
27+
onFinished: (text: string) => void,
2428
) {
25-
console.log('rscFetch :: rscId', rscId)
26-
console.log('rscFetch :: props', serializedProps)
29+
return (
30+
fetchPromise
31+
// clone the response so createFromFetch can use it (otherwise we lock the
32+
// reader) and wait for the text to be consumed so we know the stream is
33+
// finished
34+
.then((response) => response.clone().text())
35+
.then(onFinished)
36+
)
37+
}
38+
39+
function rscFetch(rscId: string, serializedProps: string) {
40+
console.log(
41+
'rscFetch :: args:\n rscId: ' +
42+
rscId +
43+
'\n serializedProps: ' +
44+
serializedProps,
45+
)
46+
const rscCacheKey = `${rscId}::${serializedProps}`
2747

28-
// TODO (RSC): The cache key should be rscId + serializedProps
29-
const cached = rscCache.get(serializedProps)
48+
const cached = rscCache.get(rscCacheKey)
3049
if (cached) {
31-
console.log('rscFetch :: cache hit for', serializedProps)
50+
console.log('rscFetch :: cache hit for', rscCacheKey)
3251
return cached
52+
} else {
53+
console.log('rscFetch :: cache miss for', rscCacheKey)
3354
}
3455

3556
const searchParams = new URLSearchParams()
3657
searchParams.set('props', serializedProps)
3758

3859
// TODO (RSC): During SSR we should not fetch (Is this function really
3960
// called during SSR?)
40-
const response = fetch(BASE_PATH + rscId + '?' + searchParams, {
61+
const responsePromise = fetch(BASE_PATH + rscId + '?' + searchParams, {
4162
headers: {
4263
'rw-rsc': '1',
4364
},
@@ -52,9 +73,16 @@ export function rscFetch(
5273
callServer: async function (rsaId: string, args: unknown[]) {
5374
// `args` is often going to be an array with just a single element,
5475
// and that element will be FormData
55-
console.log('rscFetchForClientRouter.ts :: callServer')
56-
console.log(' rsaId', rsaId)
57-
console.log(' args', args)
76+
console.log('RscFetcher :: callServer rsaId', rsaId, 'args', args)
77+
78+
// Including rsaId here to make sure the page rerenders when calling RSAs
79+
// Calling a RSA doesn't change the url (i.e. `serializedProps`), and it
80+
// also doesn't change the rscId, so React would not detect a state change
81+
// that would trigger a rerender. So we include the rsaId here to make
82+
// a new cache key that will trigger a rerender.
83+
// TODO (RSC): What happens if you call the same RSA twice in a row?
84+
// Like `increment()`
85+
const rscCacheKey = `${rscId}::${serializedProps}::${rsaId}::${new Date()}`
5886

5987
const searchParams = new URLSearchParams()
6088
searchParams.set('action_id', rsaId)
@@ -69,31 +97,37 @@ export function rscFetch(
6997
console.error('Error encoding Server Action arguments', e)
7098
}
7199

72-
const response = fetch(BASE_PATH + id + '?' + searchParams, {
100+
const responsePromise = fetch(BASE_PATH + id + '?' + searchParams, {
73101
method: 'POST',
74102
body,
75103
headers: {
76104
'rw-rsc': '1',
77105
},
78106
})
79107

108+
onStreamFinished(responsePromise, () => {
109+
updateCurrentRscCacheKey(rscCacheKey)
110+
})
111+
80112
// I'm not sure this recursive use of `options` is needed. I briefly
81113
// tried without it, and things seemed to work. But keeping it for
82114
// now, until we learn more.
83-
const data = createFromFetch(response, options)
115+
const dataPromise = createFromFetch(responsePromise, options)
84116

85-
const dataValue = await data
86-
console.log(
87-
'rscFetchForClientRuoter.ts :: callServer dataValue',
88-
dataValue,
89-
)
117+
// TODO (RSC): This is where we want to update the RSA cache, but first we
118+
// need to normalize the data that comes back from the server. We need to
119+
// always send an object with a `__rwjs__rsa_data` key and some key
120+
// for the flight data
121+
// rscCache.set(rscCacheKey, dataPromise)
122+
123+
const dataValue = await dataPromise
124+
console.log('RscFetcher :: callServer dataValue', dataValue)
90125
// TODO (RSC): Fix the types for `createFromFetch`
91126
// @ts-expect-error The type is wrong for createFromFetch
92127
const Routes = dataValue.Routes?.[0]
93128
console.log('Routes', Routes)
94129

95-
// TODO (RSC): Figure out how to trigger a rerender of the page with the
96-
// new Routes
130+
rscCache.set(rscCacheKey, Promise.resolve(Routes))
97131

98132
// TODO (RSC): Fix the types for `createFromFetch`
99133
// @ts-expect-error The type is wrong for createFromFetch. It can really
@@ -104,12 +138,14 @@ export function rscFetch(
104138
}
105139

106140
const componentPromise = createFromFetch<never, React.ReactElement>(
107-
response,
141+
responsePromise,
108142
options,
109143
)
110144

111-
rscCache.set(serializedProps, componentPromise)
145+
rscCache.set(rscCacheKey, componentPromise)
112146

147+
// TODO (RSC): Figure out if this is ever used, or if it's better to return
148+
// the cache key
113149
return componentPromise
114150
}
115151

@@ -120,25 +156,43 @@ interface Props {
120156

121157
export const RscFetcher = ({ rscId, rscProps }: Props) => {
122158
const serializedProps = JSON.stringify(rscProps)
123-
const [component, setComponent] = useState<any>(() => {
124-
console.log('RscFetcher :: useState callback')
125-
126-
return rscFetch(rscId, serializedProps)
159+
const [currentRscCacheKey, setCurrentRscCacheKey] = useState(() => {
160+
console.log('RscFetcher :: useState initial value')
161+
// Calling rscFetch here to prime the cache
162+
rscFetch(rscId, serializedProps)
163+
return `${rscId}::${serializedProps}`
127164
})
128165

129-
console.log('RscFetcher rerender rscId', rscId)
130-
console.log('RscFetcher rerender rscProps', rscProps)
166+
useEffect(() => {
167+
console.log('RscFetcher :: useEffect set updateCurrentRscCacheKey')
168+
updateCurrentRscCacheKey = (key: string) => {
169+
console.log('RscFetcher inside updateCurrentRscCacheKey', key)
131170

132-
if (!rscCache.get(serializedProps)) {
133-
rscFetch(rscId, serializedProps)
134-
}
171+
setCurrentRscCacheKey(key)
172+
}
173+
}, [])
135174

136175
useEffect(() => {
137-
console.log('RscFetcher :: useEffect rscProps')
138-
const componentPromise = rscFetch(rscId, serializedProps)
139-
console.log('componentPromise', componentPromise)
140-
setComponent(componentPromise)
176+
console.log('RscFetcher :: useEffect about to call rscFetch')
177+
// rscFetch will update rscCache with the fetched component
178+
rscFetch(rscId, serializedProps)
179+
setCurrentRscCacheKey(`${rscId}::${serializedProps}`)
141180
}, [rscId, serializedProps])
142181

143-
return component
182+
console.log(
183+
'RscFetcher :: current props\n' +
184+
' rscId: ' +
185+
rscId +
186+
'\n rscProps: ' +
187+
serializedProps,
188+
)
189+
console.log('RscFetcher :: rendering cache entry for\n' + currentRscCacheKey)
190+
191+
const component = rscCache.get(currentRscCacheKey)
192+
193+
if (!component) {
194+
throw new Error('Missing RSC cache entry for ' + currentRscCacheKey)
195+
}
196+
197+
return use(component)
144198
}

0 commit comments

Comments
 (0)