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

Handle redirects in new router #40396

Merged
merged 36 commits into from Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2d8104b
Add redirect() to new router
timneutkens Sep 9, 2022
cbca8c2
Add client-side redirect
timneutkens Sep 9, 2022
42ad643
Use object instead of error class
timneutkens Sep 9, 2022
cd5ec64
Merge branch 'canary' of github.com:vercel/next.js into add/redirect-…
timneutkens Sep 14, 2022
29d8568
Add tests for redirect in next.config.js
timneutkens Sep 14, 2022
b92b6bf
Add test for redirect in middleware
timneutkens Sep 14, 2022
4f73221
Ensure prefetch error does not crash the app
timneutkens Sep 14, 2022
6d42511
Merge branch 'canary' of github.com:vercel/next.js into add/redirect-…
timneutkens Sep 15, 2022
3d54f6b
Enable test that passes already
timneutkens Sep 15, 2022
fa28b18
Enable test that passes
timneutkens Sep 15, 2022
ca1df00
Fix type err
timneutkens Sep 15, 2022
60a952c
Merge branch 'canary' of github.com:vercel/next.js into add/redirect-…
timneutkens Sep 19, 2022
f5d3d28
Add canonicalUrl to fetchServerResponse
timneutkens Sep 19, 2022
91344e7
Fix test id
timneutkens Sep 19, 2022
2087e11
Move vercel analytics test to separate file
timneutkens Sep 19, 2022
900afec
Fix experimental_use type
timneutkens Sep 19, 2022
3f4373c
Add handling for redirected server url
timneutkens Sep 19, 2022
1ffbdd8
Skip tests that are not passing yet
timneutkens Sep 19, 2022
cdf4116
Fix ts errors
timneutkens Sep 19, 2022
7b7681a
Remove console.log
timneutkens Sep 19, 2022
4fc0cd4
Finish comment
timneutkens Sep 19, 2022
6492d39
Change infinitePromise to leverage `use`
timneutkens Sep 19, 2022
99a1bbb
Leverage use to get router instance
timneutkens Sep 19, 2022
4a437ab
Update tests
timneutkens Sep 19, 2022
4a88637
Merge branch 'canary' of github.com:vercel/next.js into add/redirect-…
timneutkens Sep 19, 2022
a17f11b
Rename file
timneutkens Sep 19, 2022
ac12057
Change page to client component
timneutkens Sep 19, 2022
c4f1b18
Only skip one test
timneutkens Sep 19, 2022
5c58f42
Merge branch 'canary' of github.com:vercel/next.js into add/redirect-…
timneutkens Sep 19, 2022
232fdc1
Add source to error handler
timneutkens Sep 20, 2022
c448661
Merge branch 'canary' of github.com:vercel/next.js into add/redirect-…
timneutkens Sep 20, 2022
d18ffca
Hide all cases of DynamicServerError
timneutkens Sep 20, 2022
373b7f5
Fix errors during build
timneutkens Sep 20, 2022
1b49efa
Fix additional build errors
timneutkens Sep 20, 2022
1da73c0
Fix tests
timneutkens Sep 20, 2022
47e1d6d
Merge branch 'canary' into add/redirect-new-router
kodiakhq[bot] Sep 20, 2022
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
28 changes: 22 additions & 6 deletions packages/next/client/components/app-router.client.tsx
Expand Up @@ -29,14 +29,22 @@ import {
} from './hooks-client-context'
import { useReducerWithReduxDevtools } from './use-reducer-with-devtools'

function urlToUrlWithoutFlightParameters(url: string): URL {
const urlWithoutFlightParameters = new URL(url, location.origin)
urlWithoutFlightParameters.searchParams.delete('__flight__')
urlWithoutFlightParameters.searchParams.delete('__flight_router_state_tree__')
urlWithoutFlightParameters.searchParams.delete('__flight_prefetch__')
return urlWithoutFlightParameters
}

/**
* Fetch the flight data for the provided url. Takes in the current router state to decide what to render server-side.
*/
export async function fetchServerResponse(
url: URL,
flightRouterState: FlightRouterState,
prefetch?: true
): Promise<[FlightData: FlightData]> {
): Promise<[FlightData: FlightData, canonicalUrlOverride: URL | undefined]> {
const flightUrl = new URL(url)
const searchParams = flightUrl.searchParams
// Enable flight response
Expand All @@ -51,9 +59,13 @@ export async function fetchServerResponse(
}

const res = await fetch(flightUrl.toString())
const canonicalUrl = res.redirected
? urlToUrlWithoutFlightParameters(res.url)
: undefined

// Handle the `fetch` readable stream that can be unwrapped by `React.use`.
const flightData: FlightData = await createFromFetch(Promise.resolve(res))
return [flightData]
return [flightData, canonicalUrl]
}

/**
Expand Down Expand Up @@ -140,11 +152,16 @@ export default function AppRouter({
* Server response that only patches the cache and tree.
*/
const changeByServerResponse = useCallback(
(previousTree: FlightRouterState, flightData: FlightData) => {
(
previousTree: FlightRouterState,
flightData: FlightData,
overrideCanonicalUrl: URL | undefined
) => {
dispatch({
type: ACTION_SERVER_PATCH,
flightData,
previousTree,
overrideCanonicalUrl,
cache: {
data: null,
subTreeData: null,
Expand Down Expand Up @@ -192,19 +209,18 @@ export default function AppRouter({

try {
// TODO-APP: handle case where history.state is not the new router history entry
const r = fetchServerResponse(
const serverResponse = await fetchServerResponse(
url,
// initialTree is used when history.state.tree is missing because the history state is set in `useEffect` below, it being missing means this is the hydration case.
window.history.state?.tree || initialTree,
true
)
const [flightData] = await r
// @ts-ignore startTransition exists
React.startTransition(() => {
dispatch({
type: ACTION_PREFETCH,
url,
flightData,
serverResponse,
})
})
} catch (err) {
Expand Down
22 changes: 22 additions & 0 deletions packages/next/client/components/infinite-promise.ts
@@ -0,0 +1,22 @@
/**
* Used to cache in createInfinitePromise
*/
let infinitePromise: Promise<void>

/**
* Create a Promise that does not resolve. This is used to suspend when data is not available yet.
*/
export function createInfinitePromise() {
if (!infinitePromise) {
// Only create the Promise once
infinitePromise = new Promise((/* resolve */) => {
// This is used to debug when the rendering is never updated.
// setTimeout(() => {
// infinitePromise = new Error('Infinite promise')
// resolve()
// }, 5000)
})
}

return infinitePromise
}
80 changes: 16 additions & 64 deletions packages/next/client/components/layout-router.client.tsx
Expand Up @@ -23,6 +23,8 @@ import {
TemplateContext,
} from '../../shared/lib/app-router-context'
import { fetchServerResponse } from './app-router.client'
import { createInfinitePromise } from './infinite-promise'

// import { matchSegment } from './match-segments'

/**
Expand Down Expand Up @@ -95,29 +97,6 @@ function walkAddRefetch(
return treeToRecreate
}

/**
* Used to cache in createInfinitePromise
*/
let infinitePromise: Promise<void> | Error

/**
* Create a Promise that does not resolve. This is used to suspend when data is not available yet.
*/
function createInfinitePromise() {
if (!infinitePromise) {
// Only create the Promise once
infinitePromise = new Promise((/* resolve */) => {
// This is used to debug when the rendering is never updated.
// setTimeout(() => {
// infinitePromise = new Error('Infinite promise')
// resolve()
// }, 5000)
})
}

return infinitePromise
}

/**
* Check if the top of the HTMLElement is in the viewport.
*/
Expand Down Expand Up @@ -237,60 +216,33 @@ export function InnerLayoutRouter({
* Flight response data
*/
// When the data has not resolved yet `use` will suspend here.
const [flightData] = use(childNode.data)
const [flightData, overrideCanonicalUrl] = use(childNode.data)

// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
window.location.href = url
return null
}

/**
* If the fast path was triggered.
* The fast path is when the returned Flight data path matches the layout segment path, then we can write the data to the cache in render instead of dispatching an action.
*/
let fastPath: boolean = false

// If there are multiple patches returned in the Flight data we need to dispatch to ensure a single render.
// if (flightData.length === 1) {
// const flightDataPath = flightData[0]

// if (segmentPathMatches(flightDataPath, segmentPath)) {
// // Ensure data is set to null as subTreeData will be set in the cache now.
// childNode.data = null
// // Last item is the subtreeData
// // TODO-APP: routerTreePatch needs to be applied to the tree, handle it in render?
// const [, /* routerTreePatch */ subTreeData] = flightDataPath.slice(-2)
// // Add subTreeData into the cache
// childNode.subTreeData = subTreeData
// // This field is required for new items
// childNode.parallelRoutes = new Map()
// fastPath = true
// }
// }

// When the fast path is not used a new action is dispatched to update the tree and cache.
if (!fastPath) {
// segmentPath from the server does not match the layout's segmentPath
childNode.data = null

// setTimeout is used to start a new transition during render, this is an intentional hack around React.
setTimeout(() => {
// @ts-ignore startTransition exists
React.startTransition(() => {
// TODO-APP: handle redirect
changeByServerResponse(fullTree, flightData)
})
// segmentPath from the server does not match the layout's segmentPath
childNode.data = null

// setTimeout is used to start a new transition during render, this is an intentional hack around React.
setTimeout(() => {
// @ts-ignore startTransition exists
React.startTransition(() => {
// TODO-APP: handle redirect
changeByServerResponse(fullTree, flightData, overrideCanonicalUrl)
})
// Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered.
throw createInfinitePromise()
}
})
// Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered.
use(createInfinitePromise())
}

// If cache node has no subTreeData and no data request we have to infinitely suspend as the data will likely flow in from another place.
// TODO-APP: double check users can't return null in a component that will kick in here.
if (!childNode.subTreeData) {
throw createInfinitePromise()
use(createInfinitePromise())
}

const subtree = (
Expand Down
15 changes: 15 additions & 0 deletions packages/next/client/components/redirect-client.ts
@@ -0,0 +1,15 @@
import React, { experimental_use as use } from 'react'
import { AppRouterContext } from '../../shared/lib/app-router-context'
import { createInfinitePromise } from './infinite-promise'

export function redirect(url: string) {
const router = use(AppRouterContext)
setTimeout(() => {
// @ts-ignore startTransition exists
React.startTransition(() => {
router.replace(url, {})
})
})
// setTimeout is used to start a new transition during render, this is an intentional hack around React.
use(createInfinitePromise())
}
9 changes: 9 additions & 0 deletions packages/next/client/components/redirect.ts
@@ -0,0 +1,9 @@
export const REDIRECT_ERROR_CODE = 'NEXT_REDIRECT'
timneutkens marked this conversation as resolved.
Show resolved Hide resolved

export function redirect(url: string) {
// eslint-disable-next-line no-throw-literal
throw {
url,
code: REDIRECT_ERROR_CODE,
}
}