Skip to content

Commit

Permalink
Add support for client references in Replies
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed May 22, 2024
1 parent 94dc45f commit d8c8855
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
// import { createFromFetch } from 'react-server-dom-webpack/client'
// // eslint-disable-next-line import/no-extraneous-dependencies
// import { encodeReply } from 'react-server-dom-webpack/client'
const { createFromFetch, encodeReply } = (
const { createFromFetch, createTemporaryReferenceSet, encodeReply } = (
!!process.env.NEXT_RUNTIME
? // eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-webpack/client.edge')
Expand Down Expand Up @@ -57,7 +57,10 @@ async function fetchServerAction(
nextUrl: ReadonlyReducerState['nextUrl'],
{ actionId, actionArgs }: ServerActionAction
): Promise<FetchServerActionResult> {
const body = await encodeReply(actionArgs)
const temporaryReferences = createTemporaryReferenceSet()
const body = await encodeReply(actionArgs, {
temporaryReferences: temporaryReferences,
})

const res = await fetch('', {
method: 'POST',
Expand Down Expand Up @@ -114,6 +117,7 @@ async function fetchServerAction(
Promise.resolve(res),
{
callServer,
temporaryReferences,
}
)

Expand Down
5 changes: 4 additions & 1 deletion packages/next/src/server/app-render/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ export async function decryptActionBoundArgs(
// This extra step ensures that the server references are recovered.
const serverModuleMap = getServerModuleMap()
const transformed = await decodeReply(
await encodeReply(deserialized),
await encodeReply(deserialized, {
// TODO: How is decryptActionBoundArgs used? Do we need to support temporary references here?
temporaryReferences: undefined,
}),
serverModuleMap
)

Expand Down
33 changes: 25 additions & 8 deletions test/e2e/app-dir/actions/app-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,25 @@ describe('app-dir action handling', () => {
await check(() => browser.elementById('count').text(), '3')
})

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

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

const logs = await browser.log()
expect(
logs.some((log) =>
log.message.includes(
'Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported.'
)
await retry(async () => {
const logs = await browser.log()
expect(logs).toEqual(
expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining(
'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.'
),
}),
])
)
).toBe(true)
})
})

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

it('should support React Elements in state', async () => {
const browser = await next.browser('/elements')

await browser.elementByCss('[type="submit"]').click()

await retry(async () => {
const form = await browser.elementByCss('form')
await expect(form.getAttribute('aria-busy')).resolves.toBe('false')
})

expect(await browser.elementByCss('output').text()).toBe('Hello, Dave!')
})

it.each(['node', 'edge'])(
'should forward action request to a worker that contains the action handler (%s)',
async (runtime) => {
Expand Down
10 changes: 10 additions & 0 deletions test/e2e/app-dir/actions/app/elements/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use server'

import * as React from 'react'

export async function action(
previousState: React.ReactElement,
formData: FormData
) {
return <p>{String(formData.get('value'))}</p>
}
23 changes: 23 additions & 0 deletions test/e2e/app-dir/actions/app/elements/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'
import * as React from 'react'
import { action } from './actions'

export default function Page() {
const [state, formAction, isPending] = React.useActionState(
action,
<p>Hello, World!</p>
)
return (
<>
<form action={formAction} aria-busy={isPending}>
<p>{isPending ? 'Pending' : 'Resolved'}</p>
<label>
Render
<input defaultValue="Hello, Dave!" type="text" name="value" />
</label>
<input type="submit" />
<output>{state}</output>
</form>
</>
)
}
5 changes: 0 additions & 5 deletions test/e2e/app-dir/actions/app/error-handling/actions.js

This file was deleted.

5 changes: 5 additions & 0 deletions test/e2e/app-dir/actions/app/error-handling/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use server'

export async function action(instance: { someProperty: string }) {
return instance.someProperty
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export default function Page() {
return (
<main>
<p>
This button will call a server action and pass something unserializable
like a class instance. We expect this action to error with a reasonable
message explaning what happened
This button will call a server action and pass something the Server
can't dot into. We expect this action to error with a reasonable message
explaning what happened.
</p>
<button
id="submit"
Expand All @@ -22,4 +22,6 @@ export default function Page() {
)
}

class Foo {}
class Foo {
someProperty = 'client'
}

0 comments on commit d8c8855

Please sign in to comment.