Skip to content

Commit

Permalink
Fix React.cache() in layout/page file (#43187)
Browse files Browse the repository at this point in the history
Co-authored-by: JJ Kasper <jj@jjsweb.site>
  • Loading branch information
timneutkens and ijjk committed Nov 22, 2022
1 parent 2743413 commit fa5dcbd
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 26 deletions.
49 changes: 24 additions & 25 deletions packages/next/server/app-render.tsx
Expand Up @@ -375,7 +375,7 @@ function useFlightResponse(
* This is only used for renderToHTML, the Flight response does not need additional wrappers.
*/
function createServerComponentRenderer(
ComponentToRender: React.ComponentType,
ComponentToRender: any,
ComponentMod: {
renderToReadableStream: any
__next_app_webpack_require__?: any
Expand Down Expand Up @@ -1442,21 +1442,21 @@ export async function renderToHTMLOrFlight(
isPrefetch && !Boolean(loaderTreeToFilter[2].loading)
? null
: // Create component tree using the slice of the loaderTree
React.createElement(
(
await createComponentTree(
// This ensures flightRouterPath is valid and filters down the tree
{
createSegmentPath: (child) => {
return createSegmentPath(child)
},
loaderTree: loaderTreeToFilter,
parentParams: currentParams,
firstItem: isFirst,
}
)
).Component
),
// @ts-expect-error TODO-APP: fix async component type
React.createElement(async () => {
const { Component } = await createComponentTree(
// This ensures flightRouterPath is valid and filters down the tree
{
createSegmentPath: (child) => {
return createSegmentPath(child)
},
loaderTree: loaderTreeToFilter,
parentParams: currentParams,
firstItem: isFirst,
}
)
return <Component />
}),
isPrefetch && !Boolean(loaderTreeToFilter[2].loading) ? null : (
<>{rscPayloadHead}</>
),
Expand Down Expand Up @@ -1529,14 +1529,6 @@ export async function renderToHTMLOrFlight(

// Below this line is handling for rendering to HTML.

// Create full component tree from root to leaf.
const { Component: ComponentTree } = await createComponentTree({
createSegmentPath: (child) => child,
loaderTree: loaderTree,
parentParams: {},
firstItem: true,
})

// AppRouter is provided by next-app-loader
const AppRouter =
ComponentMod.AppRouter as typeof import('../client/components/app-router').default
Expand Down Expand Up @@ -1579,7 +1571,14 @@ export async function renderToHTMLOrFlight(
* using Flight which can then be rendered to HTML.
*/
const ServerComponentsRenderer = createServerComponentRenderer(
() => {
async () => {
// Create full component tree from root to leaf.
const { Component: ComponentTree } = await createComponentTree({
createSegmentPath: (child) => child,
loaderTree: loaderTree,
parentParams: {},
firstItem: true,
})
const initialTree = createFlightRouterStateFromLoaderTree(loaderTree)

return (
Expand Down
16 changes: 16 additions & 0 deletions test/e2e/app-dir/app/app/react-cache/client-component/page.js
@@ -0,0 +1,16 @@
'use client'
import { cache } from 'react'

const getRandomMemoized = cache(() => Math.random())

export default function Page() {
const val1 = getRandomMemoized()
const val2 = getRandomMemoized()
return (
<>
<h1>React Cache Client Component</h1>
<p id="value-1">{val1}</p>
<p id="value-2">{val2}</p>
</>
)
}
17 changes: 17 additions & 0 deletions test/e2e/app-dir/app/app/react-cache/page.js
@@ -0,0 +1,17 @@
import Link from 'next/link'
export default function Page() {
return (
<>
<p>
<Link id="to-server-component" href="/react-cache/server-component">
To Server Component
</Link>
</p>
<p>
<Link id="to-client-component" href="/react-cache/client-component">
To Client Component
</Link>
</p>
</>
)
}
15 changes: 15 additions & 0 deletions test/e2e/app-dir/app/app/react-cache/server-component/page.js
@@ -0,0 +1,15 @@
import { cache } from 'react'

const getRandomMemoized = cache(() => Math.random())

export default function Page() {
const val1 = getRandomMemoized()
const val2 = getRandomMemoized()
return (
<>
<h1>React Cache Server Component</h1>
<p id="value-1">{val1}</p>
<p id="value-2">{val2}</p>
</>
)
}
17 changes: 17 additions & 0 deletions test/e2e/app-dir/app/app/react-fetch/page.js
@@ -0,0 +1,17 @@
import Link from 'next/link'
export default function Page() {
return (
<>
<p>
<Link id="to-server-component" href="/react-fetch/server-component">
To Server Component
</Link>
</p>
<p>
<Link id="to-client-component" href="/react-fetch/client-component">
To Client Component
</Link>
</p>
</>
)
}
18 changes: 18 additions & 0 deletions test/e2e/app-dir/app/app/react-fetch/server-component/page.js
@@ -0,0 +1,18 @@
async function getRandomMemoizedByFetch() {
const res = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random'
)
return res.text()
}

export default async function Page() {
const val1 = await getRandomMemoizedByFetch()
const val2 = await getRandomMemoizedByFetch()
return (
<>
<h1>React Fetch Server Component</h1>
<p id="value-1">{val1}</p>
<p id="value-2">{val2}</p>
</>
)
}
93 changes: 93 additions & 0 deletions test/e2e/app-dir/index.test.ts
Expand Up @@ -2165,6 +2165,99 @@ describe('app dir', () => {
})

describe('known bugs', () => {
describe('should support React cache', () => {
it('server component', async () => {
const browser = await webdriver(
next.url,
'/react-cache/server-component'
)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})

it('server component client-navigation', async () => {
const browser = await webdriver(next.url, '/react-cache')

await browser
.elementByCss('#to-server-component')
.click()
.waitForElementByCss('#value-1', 10000)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})

it('client component', async () => {
const browser = await webdriver(
next.url,
'/react-cache/client-component'
)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})

it('client component client-navigation', async () => {
const browser = await webdriver(next.url, '/react-cache')

await browser
.elementByCss('#to-client-component')
.click()
.waitForElementByCss('#value-1', 10000)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
})

describe('should support React fetch instrumentation', () => {
it('server component', async () => {
const browser = await webdriver(
next.url,
'/react-fetch/server-component'
)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})

it('server component client-navigation', async () => {
const browser = await webdriver(next.url, '/react-fetch')

await browser
.elementByCss('#to-server-component')
.click()
.waitForElementByCss('#value-1', 10000)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})

// TODO-APP: React doesn't have fetch deduping for client components yet.
it.skip('client component', async () => {
const browser = await webdriver(
next.url,
'/react-fetch/client-component'
)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})

// TODO-APP: React doesn't have fetch deduping for client components yet.
it.skip('client component client-navigation', async () => {
const browser = await webdriver(next.url, '/react-fetch')

await browser
.elementByCss('#to-client-component')
.click()
.waitForElementByCss('#value-1', 10000)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
})
it('should not share flight data between requests', async () => {
const fetches = await Promise.all(
[...new Array(5)].map(() =>
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/app-dir/rsc-errors.test.ts
Expand Up @@ -104,7 +104,7 @@ describe('app dir - rsc errors', () => {
'/server-with-errors/page-export'
)
expect(html).toContain(
'The default export is not a React Component in page: \\"/server-with-errors/page-export\\"'
'The default export is not a React Component in page:'
)
})
})

0 comments on commit fa5dcbd

Please sign in to comment.