Skip to content

Commit

Permalink
Move reducers to separate file per action (#45336)
Browse files Browse the repository at this point in the history
<!--
Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change that you're making:
-->

More cleaning / moving code around.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
timneutkens authored Jan 28, 2023
1 parent b8a1892 commit b4c80c9
Show file tree
Hide file tree
Showing 7 changed files with 712 additions and 651 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
import { CacheStates } from '../../../../shared/lib/app-router-context'
import type { FlightSegmentPath } from '../../../../server/app-render'
import { fetchServerResponse } from '../fetch-server-response'
import { createRecordFromThenable } from '../create-record-from-thenable'
import { readRecordValue } from '../read-record-value'
import { createHrefFromUrl } from '../create-href-from-url'
import { fillLazyItemsTillLeafWithHead } from '../fill-lazy-items-till-leaf-with-head'
import { fillCacheWithNewSubTreeData } from '../fill-cache-with-new-subtree-data'
import { invalidateCacheBelowFlightSegmentPath } from '../invalidate-cache-below-flight-segmentpath'
import { fillCacheWithDataProperty } from '../fill-cache-with-data-property'
import { createOptimisticTree } from '../create-optimistic-tree'
import { applyRouterStatePatchToTree } from '../apply-router-state-patch-to-tree'
import { shouldHardNavigate } from '../should-hard-navigate'
import { isNavigatingToNewRootLayout } from '../is-navigating-to-new-root-layout'
import {
NavigateAction,
ReadonlyReducerState,
ReducerState,
} from '../router-reducer-types'

export function navigateReducer(
state: ReadonlyReducerState,
action: NavigateAction
): ReducerState {
const { url, navigateType, cache, mutable, forceOptimisticNavigation } =
action
const { pathname, search } = url
const href = createHrefFromUrl(url)
const pendingPush = navigateType === 'push'

const isForCurrentTree =
JSON.stringify(mutable.previousTree) === JSON.stringify(state.tree)

if (mutable.mpaNavigation && isForCurrentTree) {
return {
// Set href.
canonicalUrl: mutable.canonicalUrlOverride
? mutable.canonicalUrlOverride
: href,
pushRef: {
pendingPush,
mpaNavigation: mutable.mpaNavigation,
},
// All navigation requires scroll and focus management to trigger.
focusAndScrollRef: { apply: false },
// Apply cache.
cache: state.cache,
prefetchCache: state.prefetchCache,
// Apply patched router state.
tree: state.tree,
}
}

// Handle concurrent rendering / strict mode case where the cache and tree were already populated.
if (mutable.patchedTree && isForCurrentTree) {
return {
// Set href.
canonicalUrl: mutable.canonicalUrlOverride
? mutable.canonicalUrlOverride
: href,
pushRef: {
pendingPush,
mpaNavigation: false,
},
// All navigation requires scroll and focus management to trigger.
focusAndScrollRef: { apply: true },
// Apply cache.
cache: mutable.useExistingCache ? state.cache : cache,
prefetchCache: state.prefetchCache,
// Apply patched router state.
tree: mutable.patchedTree,
}
}

const prefetchValues = state.prefetchCache.get(href)
if (prefetchValues) {
// The one before last item is the router state tree patch
const { flightData, tree: newTree, canonicalUrlOverride } = prefetchValues

// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return {
canonicalUrl: flightData,
// Enable mpaNavigation
pushRef: { pendingPush: true, mpaNavigation: true },
// Don't apply scroll and focus management.
focusAndScrollRef: { apply: false },
cache: state.cache,
prefetchCache: state.prefetchCache,
tree: state.tree,
}
}

if (newTree !== null) {
mutable.previousTree = state.tree
mutable.patchedTree = newTree
mutable.mpaNavigation = isNavigatingToNewRootLayout(state.tree, newTree)

if (newTree === null) {
throw new Error('SEGMENT MISMATCH')
}

const canonicalUrlOverrideHrefVal = canonicalUrlOverride
? createHrefFromUrl(canonicalUrlOverride)
: undefined
if (canonicalUrlOverrideHrefVal) {
mutable.canonicalUrlOverride = canonicalUrlOverrideHrefVal
}
mutable.mpaNavigation = isNavigatingToNewRootLayout(state.tree, newTree)

// TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths.
const flightDataPath = flightData[0]
const flightSegmentPath = flightDataPath.slice(
0,
-3
) as unknown as FlightSegmentPath
// The one before last item is the router state tree patch
const [treePatch, subTreeData, head] = flightDataPath.slice(-3)

// Handles case where prefetch only returns the router tree patch without rendered components.
if (subTreeData !== null) {
if (flightDataPath.length === 3) {
cache.status = CacheStates.READY
cache.subTreeData = subTreeData
cache.parallelRoutes = new Map()
fillLazyItemsTillLeafWithHead(cache, state.cache, treePatch, head)
} else {
cache.status = CacheStates.READY
// Copy subTreeData for the root node of the cache.
cache.subTreeData = state.cache.subTreeData
// Create a copy of the existing cache with the subTreeData applied.
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath)
}
}

const hardNavigate =
// TODO-APP: Revisit if this is correct.
search !== location.search ||
shouldHardNavigate(
// TODO-APP: remove ''
['', ...flightSegmentPath],
state.tree
)

if (hardNavigate) {
cache.status = CacheStates.READY
// Copy subTreeData for the root node of the cache.
cache.subTreeData = state.cache.subTreeData

invalidateCacheBelowFlightSegmentPath(
cache,
state.cache,
flightSegmentPath
)
// Ensure the existing cache value is used when the cache was not invalidated.
} else if (subTreeData === null) {
mutable.useExistingCache = true
}

const canonicalUrlOverrideHref = canonicalUrlOverride
? createHrefFromUrl(canonicalUrlOverride)
: undefined

if (canonicalUrlOverrideHref) {
mutable.canonicalUrlOverride = canonicalUrlOverrideHref
}

return {
// Set href.
canonicalUrl: canonicalUrlOverrideHref
? canonicalUrlOverrideHref
: href,
// Set pendingPush.
pushRef: { pendingPush, mpaNavigation: false },
// All navigation requires scroll and focus management to trigger.
focusAndScrollRef: { apply: true },
// Apply patched cache.
cache: mutable.useExistingCache ? state.cache : cache,
prefetchCache: state.prefetchCache,
// Apply patched tree.
tree: newTree,
}
}
}

// When doing a hard push there can be two cases: with optimistic tree and without
// The with optimistic tree case only happens when the layouts have a loading state (loading.js)
// The without optimistic tree case happens when there is no loading state, in that case we suspend in this reducer

// forceOptimisticNavigation is used for links that have `prefetch={false}`.
if (forceOptimisticNavigation) {
const segments = pathname.split('/')
// TODO-APP: figure out something better for index pages
segments.push('')

// Optimistic tree case.
// If the optimistic tree is deeper than the current state leave that deeper part out of the fetch
const optimisticTree = createOptimisticTree(segments, state.tree, false)

// Copy subTreeData for the root node of the cache.
cache.status = CacheStates.READY
cache.subTreeData = state.cache.subTreeData

// Copy existing cache nodes as far as possible and fill in `data` property with the started data fetch.
// The `data` property is used to suspend in layout-router during render if it hasn't resolved yet by the time it renders.
const res = fillCacheWithDataProperty(
cache,
state.cache,
// TODO-APP: segments.slice(1) strips '', we can get rid of '' altogether.
segments.slice(1),
() => fetchServerResponse(url, optimisticTree)
)

// If optimistic fetch couldn't happen it falls back to the non-optimistic case.
if (!res?.bailOptimistic) {
mutable.previousTree = state.tree
mutable.patchedTree = optimisticTree
mutable.mpaNavigation = isNavigatingToNewRootLayout(
state.tree,
optimisticTree
)
return {
// Set href.
canonicalUrl: href,
// Set pendingPush.
pushRef: { pendingPush, mpaNavigation: false },
// All navigation requires scroll and focus management to trigger.
focusAndScrollRef: { apply: true },
// Apply patched cache.
cache: cache,
prefetchCache: state.prefetchCache,
// Apply optimistic tree.
tree: optimisticTree,
}
}
}

// Below is the not-optimistic case. Data is fetched at the root and suspended there without a suspense boundary.

// If no in-flight fetch at the top, start it.
if (!cache.data) {
cache.data = createRecordFromThenable(fetchServerResponse(url, state.tree))
}

// Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves.
const [flightData, canonicalUrlOverride] = readRecordValue(cache.data!)

// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return {
canonicalUrl: flightData,
// Enable mpaNavigation
pushRef: { pendingPush: true, mpaNavigation: true },
// Don't apply scroll and focus management.
focusAndScrollRef: { apply: false },
cache: state.cache,
prefetchCache: state.prefetchCache,
tree: state.tree,
}
}

// Remove cache.data as it has been resolved at this point.
cache.data = null

// TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths.
const flightDataPath = flightData[0]

// The one before last item is the router state tree patch
const [treePatch, subTreeData, head] = flightDataPath.slice(-3)

// Path without the last segment, router state, and the subTreeData
const flightSegmentPath = flightDataPath.slice(0, -4)

// Create new tree based on the flightSegmentPath and router state patch
const newTree = applyRouterStatePatchToTree(
// TODO-APP: remove ''
['', ...flightSegmentPath],
state.tree,
treePatch
)

if (newTree === null) {
throw new Error('SEGMENT MISMATCH')
}

const canonicalUrlOverrideHref = canonicalUrlOverride
? createHrefFromUrl(canonicalUrlOverride)
: undefined
if (canonicalUrlOverrideHref) {
mutable.canonicalUrlOverride = canonicalUrlOverrideHref
}
mutable.previousTree = state.tree
mutable.patchedTree = newTree
mutable.mpaNavigation = isNavigatingToNewRootLayout(state.tree, newTree)

if (flightDataPath.length === 3) {
cache.status = CacheStates.READY
cache.subTreeData = subTreeData
fillLazyItemsTillLeafWithHead(cache, state.cache, treePatch, head)
} else {
// Copy subTreeData for the root node of the cache.
cache.status = CacheStates.READY
cache.subTreeData = state.cache.subTreeData
// Create a copy of the existing cache with the subTreeData applied.
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath)
}

return {
// Set href.
canonicalUrl: canonicalUrlOverrideHref ? canonicalUrlOverrideHref : href,
// Set pendingPush.
pushRef: { pendingPush, mpaNavigation: false },
// All navigation requires scroll and focus management to trigger.
focusAndScrollRef: { apply: true },
// Apply patched cache.
cache: cache,
prefetchCache: state.prefetchCache,
// Apply patched tree.
tree: newTree,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { applyRouterStatePatchToTree } from '../apply-router-state-patch-to-tree'
import { createHrefFromUrl } from '../create-href-from-url'
import {
PrefetchAction,
ReducerState,
ReadonlyReducerState,
} from '../router-reducer-types'

export function prefetchReducer(
state: ReadonlyReducerState,
action: PrefetchAction
): ReducerState {
const { url, serverResponse } = action
const [flightData, canonicalUrlOverride] = serverResponse

if (typeof flightData === 'string') {
return state
}

const href = createHrefFromUrl(url)

// TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths.
const flightDataPath = flightData[0]

// The one before last item is the router state tree patch
const [treePatch] = flightDataPath.slice(-3)

const flightSegmentPath = flightDataPath.slice(0, -3)

const newTree = applyRouterStatePatchToTree(
// TODO-APP: remove ''
['', ...flightSegmentPath],
state.tree,
treePatch
)

// Patch did not apply correctly
if (newTree === null) {
return state
}

// Create new tree based on the flightSegmentPath and router state patch
state.prefetchCache.set(href, {
flightData,
// Create new tree based on the flightSegmentPath and router state patch
tree: newTree,
canonicalUrlOverride,
})

return state
}
Loading

0 comments on commit b4c80c9

Please sign in to comment.