Skip to content

Commit d8c8855

Browse files
committed
Add support for client references in Replies
1 parent 94dc45f commit d8c8855

File tree

8 files changed

+79
-20
lines changed

8 files changed

+79
-20
lines changed

packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
// import { createFromFetch } from 'react-server-dom-webpack/client'
1515
// // eslint-disable-next-line import/no-extraneous-dependencies
1616
// import { encodeReply } from 'react-server-dom-webpack/client'
17-
const { createFromFetch, encodeReply } = (
17+
const { createFromFetch, createTemporaryReferenceSet, encodeReply } = (
1818
!!process.env.NEXT_RUNTIME
1919
? // eslint-disable-next-line import/no-extraneous-dependencies
2020
require('react-server-dom-webpack/client.edge')
@@ -57,7 +57,10 @@ async function fetchServerAction(
5757
nextUrl: ReadonlyReducerState['nextUrl'],
5858
{ actionId, actionArgs }: ServerActionAction
5959
): Promise<FetchServerActionResult> {
60-
const body = await encodeReply(actionArgs)
60+
const temporaryReferences = createTemporaryReferenceSet()
61+
const body = await encodeReply(actionArgs, {
62+
temporaryReferences: temporaryReferences,
63+
})
6164

6265
const res = await fetch('', {
6366
method: 'POST',
@@ -114,6 +117,7 @@ async function fetchServerAction(
114117
Promise.resolve(res),
115118
{
116119
callServer,
120+
temporaryReferences,
117121
}
118122
)
119123

packages/next/src/server/app-render/encryption.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,10 @@ export async function decryptActionBoundArgs(
120120
// This extra step ensures that the server references are recovered.
121121
const serverModuleMap = getServerModuleMap()
122122
const transformed = await decodeReply(
123-
await encodeReply(deserialized),
123+
await encodeReply(deserialized, {
124+
// TODO: How is decryptActionBoundArgs used? Do we need to support temporary references here?
125+
temporaryReferences: undefined,
126+
}),
124127
serverModuleMap
125128
)
126129

test/e2e/app-dir/actions/app-action.test.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,25 @@ describe('app-dir action handling', () => {
4545
await check(() => browser.elementById('count').text(), '3')
4646
})
4747

48-
it('should report errors with bad inputs correctly', async () => {
48+
it('should report errors with bad input handling on the server', async () => {
4949
const browser = await next.browser('/error-handling', {
5050
pushErrorAsConsoleLog: true,
5151
})
5252

5353
await browser.elementByCss('#submit').click()
5454

55-
const logs = await browser.log()
56-
expect(
57-
logs.some((log) =>
58-
log.message.includes(
59-
'Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported.'
60-
)
55+
await retry(async () => {
56+
const logs = await browser.log()
57+
expect(logs).toEqual(
58+
expect.arrayContaining([
59+
expect.objectContaining({
60+
message: expect.stringContaining(
61+
'Cannot access someProperty on the server. You cannot dot into a temporary client reference from a server component. You can only pass the value through to the client.'
62+
),
63+
}),
64+
])
6165
)
62-
).toBe(true)
66+
})
6367
})
6468

6569
it('should support headers and cookies', async () => {
@@ -561,6 +565,19 @@ describe('app-dir action handling', () => {
561565
).toBe(true)
562566
})
563567

568+
it('should support React Elements in state', async () => {
569+
const browser = await next.browser('/elements')
570+
571+
await browser.elementByCss('[type="submit"]').click()
572+
573+
await retry(async () => {
574+
const form = await browser.elementByCss('form')
575+
await expect(form.getAttribute('aria-busy')).resolves.toBe('false')
576+
})
577+
578+
expect(await browser.elementByCss('output').text()).toBe('Hello, Dave!')
579+
})
580+
564581
it.each(['node', 'edge'])(
565582
'should forward action request to a worker that contains the action handler (%s)',
566583
async (runtime) => {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use server'
2+
3+
import * as React from 'react'
4+
5+
export async function action(
6+
previousState: React.ReactElement,
7+
formData: FormData
8+
) {
9+
return <p>{String(formData.get('value'))}</p>
10+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client'
2+
import * as React from 'react'
3+
import { action } from './actions'
4+
5+
export default function Page() {
6+
const [state, formAction, isPending] = React.useActionState(
7+
action,
8+
<p>Hello, World!</p>
9+
)
10+
return (
11+
<>
12+
<form action={formAction} aria-busy={isPending}>
13+
<p>{isPending ? 'Pending' : 'Resolved'}</p>
14+
<label>
15+
Render
16+
<input defaultValue="Hello, Dave!" type="text" name="value" />
17+
</label>
18+
<input type="submit" />
19+
<output>{state}</output>
20+
</form>
21+
</>
22+
)
23+
}

test/e2e/app-dir/actions/app/error-handling/actions.js

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use server'
2+
3+
export async function action(instance: { someProperty: string }) {
4+
return instance.someProperty
5+
}

test/e2e/app-dir/actions/app/error-handling/page.js renamed to test/e2e/app-dir/actions/app/error-handling/page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ export default function Page() {
66
return (
77
<main>
88
<p>
9-
This button will call a server action and pass something unserializable
10-
like a class instance. We expect this action to error with a reasonable
11-
message explaning what happened
9+
This button will call a server action and pass something the Server
10+
can't dot into. We expect this action to error with a reasonable message
11+
explaning what happened.
1212
</p>
1313
<button
1414
id="submit"
@@ -22,4 +22,6 @@ export default function Page() {
2222
)
2323
}
2424

25-
class Foo {}
25+
class Foo {
26+
someProperty = 'client'
27+
}

0 commit comments

Comments
 (0)