diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index c02cec2a5..dea0ed267 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -913,6 +913,58 @@ function defineTest(f: Fixture) { ) }) + test('React.cache API', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + + // Verify React.cache section is present + const reactCacheSection = page.getByTestId('test-react-cache') + await expect(reactCacheSection).toBeVisible() + + // Test cached data fetching - multiple components with same ID should share cache + const user1Elements = page.getByTestId('react-cache-fetch-user-1') + await expect(user1Elements).toHaveCount(2) + + // Both user-1 elements should have the same cached timestamp + const user1Texts = await user1Elements.allTextContents() + expect(user1Texts[0]).toEqual(user1Texts[1]) // Should be identical due to caching + + // Different user ID should have different data + const user2Element = page.getByTestId('react-cache-fetch-user-2') + await expect(user2Element).toBeVisible() + const user2Text = await user2Element.textContent() + expect(user2Text).toContain('User user-2') + expect(user2Text).not.toEqual(user1Texts[0]) + + // Test cached expensive computation + const testDataElements = page.getByTestId( + 'react-cache-computation-test-data', + ) + await expect(testDataElements).toHaveCount(2) + + // Both test-data elements should have identical results + const computationTexts = await testDataElements.allTextContents() + expect(computationTexts[0]).toEqual(computationTexts[1]) + expect(computationTexts[0]).toContain('Computed result for test-data') + + // Different data should produce different results + const differentDataElement = page.getByTestId( + 'react-cache-computation-different-data', + ) + await expect(differentDataElement).toBeVisible() + const differentDataText = await differentDataElement.textContent() + expect(differentDataText).toContain('Computed result for different-data') + expect(differentDataText).not.toEqual(computationTexts[0]) + + // Verify React.cache is working by checking that function call counts are present + // and that caching behavior is working (identical results for same inputs) + expect(user1Texts[0]).toMatch(/Fetch calls: \d+/) // Should show some number of fetch calls + expect(computationTexts[0]).toMatch(/Computation calls: \d+/) // Should show some number of computation calls + + // The key test: verify that React.cache is producing identical results for identical inputs + // This is the core functionality we want to test + }) + test('hydration mismatch', async ({ page }) => { const errors: Error[] = [] page.on('pageerror', (error) => { diff --git a/packages/plugin-rsc/examples/basic/src/routes/react-cache/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/react-cache/server.tsx new file mode 100644 index 000000000..6b4654161 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/react-cache/server.tsx @@ -0,0 +1,72 @@ +import React from 'react' + +export function TestReactCache() { + return ( +
+
+

React.cache API Test

+ + + + + + +
+
+ ) +} + +// Global counters for tracking function calls +let fetchUserDataCallCount = 0 +let expensiveComputationCallCount = 0 + +// Mock data fetching function +async function fetchUserData(id: string) { + fetchUserDataCallCount++ + await new Promise((resolve) => setTimeout(resolve, 100)) // Simulate network delay + return { + id, + name: `User ${id}`, + email: `${id}@example.com`, + timestamp: new Date().toISOString(), + } +} + +// Mock expensive computation +function expensiveComputation(data: string) { + expensiveComputationCallCount++ + // Simulate expensive computation + let result = 0 + for (let i = 0; i < 1000000; i++) { + result += i + } + return `Computed result for ${data}: ${result}` +} + +// Create cached versions using React.cache +const cachedFetchUserData = React.cache(fetchUserData) +const cachedExpensiveComputation = React.cache(expensiveComputation) + +async function ReactCacheDataFetch({ id }: { id: string }) { + const userData = await cachedFetchUserData(id) + return ( +
+ User: {userData.name} ({userData.email}) +
+ + Fetch calls: {fetchUserDataCallCount} | Cached at: {userData.timestamp} + +
+ ) +} + +function ReactCacheExpensiveComputation({ data }: { data: string }) { + const result = cachedExpensiveComputation(data) + return ( +
+ Result: {result} +
+ Computation calls: {expensiveComputationCallCount} +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/root.tsx b/packages/plugin-rsc/examples/basic/src/routes/root.tsx index f679c362e..d7dcbf098 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/root.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/root.tsx @@ -27,6 +27,7 @@ import { TestTailwindClient } from './tailwind/client' import { TestTailwindServer } from './tailwind/server' import { TestTemporaryReference } from './temporary-reference/client' import { TestUseCache } from './use-cache/server' +import { TestReactCache } from './react-cache/server' import { TestHydrationMismatch } from './hydration-mismatch/server' import { TestBrowserOnly } from './browser-only/client' import { TestTransitiveCjsClient } from './deps/transitive-cjs/client' @@ -73,6 +74,7 @@ export function Root(props: { url: URL }) { + )