diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index a2c9e4c20..9952729d5 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -961,6 +961,73 @@ function defineTest(f: Fixture) { ) }) + test('React.cache API availability', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + + // Test that the React.cache component renders + await expect(page.getByTestId('react-cache-test')).toContainText( + 'React.cache Test', + ) + + // Test React.cache and React.use are available + await expect(page.getByTestId('react-cache-available')).toContainText( + 'React.cache available: Yes', + ) + await expect(page.getByTestId('react-use-available')).toContainText( + 'React.use available: Yes', + ) + + // Test that we can create cached functions + await expect(page.getByTestId('api-test-result')).toContainText( + 'Success - created cached function of type: function', + ) + }) + + test('React.cache synchronous behavior', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + + // Test synchronous cache behavior + const result1 = await page.getByTestId('sync-result1').textContent() + const result2 = await page.getByTestId('sync-result2').textContent() + + // Verify both calls return the same value (indicating caching) + expect(result1).toBe(result2) + await expect(page.getByTestId('sync-results-equal')).toContainText( + 'Results equal: true', + ) + }) + + test('React.cache with async operations', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + + // Wait for suspense to resolve + await expect(page.getByTestId('async-result')).toBeVisible() + + // Verify async cache result loads + await expect(page.getByTestId('async-result')).toContainText('Async result') + }) + + test('React.cache re-render behavior', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + + // Get initial call count + const initialCallCount = await page + .getByTestId('sync-call-count') + .textContent() + + // Force a re-render + await page.getByTestId('cache-test-rerender').click() + + // Verify call count (behavior may vary between React versions) + const newCallCount = await page.getByTestId('sync-call-count').textContent() + expect(newCallCount).toBeTruthy() + expect(initialCallCount).toBeTruthy() + }) + test('hydration mismatch', async ({ page }) => { const errors: Error[] = [] page.on('pageerror', (error) => { diff --git a/packages/plugin-rsc/examples/basic/src/routes/react-cache/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/react-cache/client.tsx new file mode 100644 index 000000000..5e6dcff63 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/react-cache/client.tsx @@ -0,0 +1,127 @@ +'use client' + +import React, { useState } from 'react' + +let callCount = 0 + +// Create a cached function that should demonstrate React.cache behavior +const cacheFn = React.cache(() => { + callCount++ + return `Called ${callCount} times` +}) + +// Create an async cached function for demonstration +const asyncCacheFn = React.cache(async () => { + await new Promise((r) => setTimeout(r, 10)) + return `Async result` +}) + +export function ReactCacheTest() { + const [renderKey, setRenderKey] = useState(0) + + return ( +
+

React.cache Test

+
React version: {React.version}
+
+ React.cache available:{' '} + {typeof React.cache === 'function' ? 'Yes' : 'No'} +
+
+ React.use available: {typeof React.use === 'function' ? 'Yes' : 'No'} +
+ + + + + + {/* Test API availability */} + + + {/* Test synchronous cache behavior */} + + + {/* Test async cache with error boundary */} + Loading async test...
} + > + + + + ) +} + +function ApiAvailabilityTest() { + let result = 'unknown' + try { + const testCache = React.cache(() => 'test') + result = `Success - created cached function of type: ${typeof testCache}` + } catch (error) { + result = `Error: ${error instanceof Error ? error.message : String(error)}` + } + + return ( +
+

API Availability Test

+
Cache creation: {result}
+
+ ) +} + +function SyncCacheTest() { + // Call the cached function multiple times within the same render + const result1 = cacheFn() + const result2 = cacheFn() + + return ( +
+

Synchronous Cache Test

+
First call: {result1}
+
Second call: {result2}
+
+ Results equal: {result1 === result2 ? 'true' : 'false'} +
+
Total function calls: {callCount}
+
+ ) +} + +function AsyncCacheTest() { + // Use React.use to consume async cached function - don't use try/catch + const asyncResult = React.use(asyncCacheFn()) + + return ( +
+

Async Cache Test

+
Async result: {asyncResult}
+
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/root.tsx b/packages/plugin-rsc/examples/basic/src/routes/root.tsx index 1bda17eed..6b32efe20 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/root.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/root.tsx @@ -31,6 +31,7 @@ import { TestHydrationMismatch } from './hydration-mismatch/server' import { TestBrowserOnly } from './browser-only/client' import { TestTransitiveCjsClient } from './deps/transitive-cjs/client' import TestDepCssInServer from '@vitejs/test-dep-css-in-server/server' +import { ReactCacheTest } from './react-cache/client' export function Root(props: { url: URL }) { return ( @@ -75,6 +76,7 @@ export function Root(props: { url: URL }) { + )