Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix React.cache() in layout/page file #43187

Merged
merged 7 commits into from Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'
ijjk marked this conversation as resolved.
Show resolved Hide resolved
)
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:'
)
})
})