Skip to content

Commit 232be7b

Browse files
authored
fix(rsc): handle added/removed "use client" during dev (#750)
1 parent 596c76b commit 232be7b

File tree

5 files changed

+90
-2
lines changed

5 files changed

+90
-2
lines changed

packages/plugin-rsc/e2e/basic.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,48 @@ function defineTest(f: Fixture) {
431431
await page.reload()
432432
await expect(page.getByText('ok (test-shared)')).toBeVisible()
433433
})
434+
435+
test('hmr switch server to client', async ({ page }) => {
436+
await page.goto(f.url())
437+
await waitForHydration(page)
438+
await using _ = await expectNoReload(page)
439+
440+
await expect(page.getByTestId('test-hmr-switch-server')).toContainText(
441+
'(useState: false)',
442+
)
443+
const editor = f.createEditor('src/routes/hmr-switch/server.tsx')
444+
editor.edit((s) => `"use client";\n` + s)
445+
await expect(page.getByTestId('test-hmr-switch-server')).toContainText(
446+
'(useState: true)',
447+
)
448+
449+
await page.waitForTimeout(100)
450+
editor.reset()
451+
await expect(page.getByTestId('test-hmr-switch-server')).toContainText(
452+
'(useState: false)',
453+
)
454+
})
455+
456+
test('hmr switch client to server', async ({ page }) => {
457+
await page.goto(f.url())
458+
await waitForHydration(page)
459+
await using _ = await expectNoReload(page)
460+
461+
await expect(page.getByTestId('test-hmr-switch-client')).toContainText(
462+
'(useState: true)',
463+
)
464+
const editor = f.createEditor('src/routes/hmr-switch/client.tsx')
465+
editor.edit((s) => s.replace(`'use client'`, ''))
466+
await expect(page.getByTestId('test-hmr-switch-client')).toContainText(
467+
'(useState: false)',
468+
)
469+
470+
await page.waitForTimeout(100)
471+
editor.reset()
472+
await expect(page.getByTestId('test-hmr-switch-client')).toContainText(
473+
'(useState: true)',
474+
)
475+
})
434476
})
435477

436478
test('css @js', async ({ page }) => {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use client'
2+
3+
import React from 'react'
4+
5+
export function TestHmrSwitchClient() {
6+
return (
7+
<div data-testid="test-hmr-switch-client">
8+
test-hmr-switch-client (useState: {String(!!React.useState)})
9+
</div>
10+
)
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
3+
export function TestHmrSwitchServer() {
4+
return (
5+
<div data-testid="test-hmr-switch-server">
6+
test-hmr-switch-server (useState: {String(!!React.useState)})
7+
</div>
8+
)
9+
}

packages/plugin-rsc/examples/basic/src/routes/root.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { TestHmrSharedAtomic } from './hmr-shared/atomic/server'
3838
import { TestCssQueries } from './css-queries/server'
3939
import { TestImportMetaGlob } from './import-meta-glob/server'
4040
import { TestAssetsServer } from './assets/server'
41+
import { TestHmrSwitchServer } from './hmr-switch/server'
42+
import { TestHmrSwitchClient } from './hmr-switch/client'
4143

4244
export function Root(props: { url: URL }) {
4345
return (
@@ -65,6 +67,8 @@ export function Root(props: { url: URL }) {
6567
<TestHmrSharedServer />
6668
<TestHmrSharedClient />
6769
<TestHmrSharedAtomic />
70+
<TestHmrSwitchServer />
71+
<TestHmrSwitchClient />
6872
<TestTemporaryReference />
6973
<TestServerActionError />
7074
<TestReplayConsoleLogs url={props.url} />

packages/plugin-rsc/src/plugin.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,22 @@ export default function vitePluginRsc(
453453
const ids = ctx.modules.map((mod) => mod.id).filter((v) => v !== null)
454454
if (ids.length === 0) return
455455

456+
// handle client -> server switch (i.e. "use client" removal)
457+
// by eagerly transforming new module on "rsc" environment.
458+
if (this.environment.name === 'rsc') {
459+
for (const mod of ctx.modules) {
460+
if (
461+
mod.type === 'js' &&
462+
mod.id &&
463+
mod.id in manager.clientReferenceMetaMap
464+
) {
465+
try {
466+
await this.environment.transformRequest(mod.url)
467+
} catch {}
468+
}
469+
}
470+
}
471+
456472
// a shared component/module will have `isInsideClientBoundary = false` on `rsc` environment
457473
// and `isInsideClientBoundary = true` on `client` environment,
458474
// which means both server hmr and client hmr will be triggered.
@@ -1009,10 +1025,16 @@ function vitePluginUseClient(
10091025
name: 'rsc:use-client',
10101026
async transform(code, id) {
10111027
if (this.environment.name !== serverEnvironmentName) return
1012-
if (!code.includes('use client')) return
1028+
if (!code.includes('use client')) {
1029+
delete manager.clientReferenceMetaMap[id]
1030+
return
1031+
}
10131032

10141033
const ast = await parseAstAsync(code)
1015-
if (!hasDirective(ast.body, 'use client')) return
1034+
if (!hasDirective(ast.body, 'use client')) {
1035+
delete manager.clientReferenceMetaMap[id]
1036+
return
1037+
}
10161038

10171039
let importId: string
10181040
let referenceKey: string

0 commit comments

Comments
 (0)