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

Add refreshing of Server Components #38508

Merged
merged 4 commits into from Jul 11, 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
47 changes: 28 additions & 19 deletions packages/next/client/components/app-router.client.tsx
Expand Up @@ -37,14 +37,8 @@ export function fetchServerResponse(
}

// TODO: move this back into AppRouter
let initialCache: CacheNode =
typeof window === 'undefined'
? null!
: {
data: null,
subTreeData: null,
parallelRoutes: new Map(),
}
let initialParallelRoutes: CacheNode['parallelRoutes'] =
typeof window === 'undefined' ? null! : new Map()

export default function AppRouter({
initialTree,
Expand All @@ -61,20 +55,18 @@ export default function AppRouter({
typeof reducer
>(reducer, {
tree: initialTree,
cache:
typeof window === 'undefined'
? {
data: null,
subTreeData: null,
parallelRoutes: new Map(),
}
: initialCache,
cache: {
data: null,
subTreeData: children,
parallelRoutes:
typeof window === 'undefined' ? new Map() : initialParallelRoutes,
},
pushRef: { pendingPush: false, mpaNavigation: false },
canonicalUrl: initialCanonicalUrl,
})

useEffect(() => {
initialCache = null!
initialParallelRoutes = null!
}, [])

const { query, pathname } = React.useMemo(() => {
Expand Down Expand Up @@ -157,6 +149,24 @@ export default function AppRouter({
navigate(href, 'hard', 'push')
})
},
reload: () => {
// @ts-ignore startTransition exists
React.startTransition(() => {
dispatch({
type: 'reload',
payload: {
// TODO: revisit if this needs to be passed.
url: new URL(window.location.href),
cache: {
data: null,
subTreeData: null,
parallelRoutes: new Map(),
},
mutable: {},
},
})
})
},
}

return routerInstance
Expand Down Expand Up @@ -219,7 +229,6 @@ export default function AppRouter({
window.removeEventListener('popstate', onPopState)
}
}, [onPopState])

return (
<PathnameContext.Provider value={pathname}>
<QueryContext.Provider value={query}>
Expand All @@ -239,7 +248,7 @@ export default function AppRouter({
url: canonicalUrl,
}}
>
{children}
{cache.subTreeData}
{hotReloader}
</AppTreeContext.Provider>
</AppRouterContext.Provider>
Expand Down
22 changes: 19 additions & 3 deletions packages/next/client/components/hot-reloader.client.tsx
Expand Up @@ -8,6 +8,7 @@ import {
} from 'next/dist/compiled/@next/react-dev-overlay/dist/client'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import formatWebpackMessages from '../dev/error-overlay/format-webpack-messages'
import { useRouter } from './hooks-client'

function getSocketProtocol(assetPrefix: string): string {
let protocol = window.location.protocol
Expand Down Expand Up @@ -177,7 +178,11 @@ function performFullReload(err: any, sendMessage: any) {
window.location.reload()
}

function processMessage(e: any, sendMessage: any) {
function processMessage(
e: any,
sendMessage: any,
router: ReturnType<typeof useRouter>
) {
const obj = JSON.parse(e.data)

switch (obj.action) {
Expand Down Expand Up @@ -292,6 +297,16 @@ function processMessage(e: any, sendMessage: any) {
}
return
}
// TODO: make server component change more granular
case 'serverComponentChanges': {
sendMessage(
JSON.stringify({
event: 'server-component-reload-page',
clientId: __nextDevClientId,
})
)
return router.reload()
}
case 'reloadPage': {
sendMessage(
JSON.stringify({
Expand Down Expand Up @@ -367,6 +382,7 @@ function processMessage(e: any, sendMessage: any) {

export default function HotReload({ assetPrefix }: { assetPrefix: string }) {
const { tree } = useContext(FullAppTreeContext)
const router = useRouter()

const webSocketRef = useRef<WebSocket>()
const sendMessage = useCallback((data) => {
Expand Down Expand Up @@ -424,7 +440,7 @@ export default function HotReload({ assetPrefix }: { assetPrefix: string }) {
}

try {
processMessage(event, sendMessage)
processMessage(event, sendMessage, router)
} catch (ex) {
console.warn('Invalid HMR message: ' + event.data + '\n', ex)
}
Expand All @@ -437,7 +453,7 @@ export default function HotReload({ assetPrefix }: { assetPrefix: string }) {
return () =>
webSocketRef.current &&
webSocketRef.current.removeEventListener('message', handler)
}, [sendMessage])
}, [sendMessage, router])
// useEffect(() => {
// const interval = setInterval(function () {
// if (
Expand Down
98 changes: 98 additions & 0 deletions packages/next/client/components/reducer.ts
Expand Up @@ -209,6 +209,18 @@ const walkTreeWithFlightDataPath = (
treePatch: FlightRouterState
): FlightRouterState => {
const [segment, parallelRoutes, url] = flightRouterState

// Root refresh
if (flightSegmentPath.length === 1) {
const tree: FlightRouterState = [...treePatch]

if (url) {
tree.push(url)
}

return tree
}

const [currentSegment, parallelRouteKey] = flightSegmentPath

// Tree path returned from the server should always match up with the current tree in the browser
Expand Down Expand Up @@ -250,6 +262,17 @@ type AppRouterState = {
export function reducer(
state: AppRouterState,
action:
| {
type: 'reload'
payload: {
url: URL
cache: CacheNode
mutable: {
previousTree?: FlightRouterState
patchedTree?: FlightRouterState
}
}
}
| {
type: 'navigate'
payload: {
Expand Down Expand Up @@ -398,6 +421,7 @@ export function reducer(
mutable.previousTree = state.tree
mutable.patchedTree = newTree

cache.subTreeData = state.cache.subTreeData
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath)

return {
Expand Down Expand Up @@ -448,6 +472,7 @@ export function reducer(
treePatch
)

cache.subTreeData = state.cache.subTreeData
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath)

return {
Expand All @@ -458,5 +483,78 @@ export function reducer(
}
}

if (action.type === 'reload') {
const { url, cache, mutable } = action.payload
const href = url.pathname + url.search + url.hash
const pendingPush = false

// 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

if (
mutable.patchedTree &&
JSON.stringify(mutable.previousTree) === JSON.stringify(state.tree)
) {
return {
canonicalUrl: href,
pushRef: { pendingPush, mpaNavigation: false },
cache: cache,
tree: mutable.patchedTree,
}
}

if (!cache.data) {
cache.data = fetchServerResponse(url, [
state.tree[0],
state.tree[1],
state.tree[2],
'refetch',
])
}
const flightData = cache.data.readRoot()

// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return {
canonicalUrl: flightData,
pushRef: { pendingPush: true, mpaNavigation: true },
cache: state.cache,
tree: state.tree,
}
}

cache.data = null

// TODO: ensure flightDataPath does not have "" as first item
const flightDataPath = flightData[0]

if (flightDataPath.length !== 2) {
// TODO: handle this case better
console.log('RELOAD FAILED')
return state
}

const [treePatch, subTreeData] = flightDataPath.slice(-2)
const newTree = walkTreeWithFlightDataPath(
// TODO: remove ''
[''],
state.tree,
treePatch
)

mutable.previousTree = state.tree
mutable.patchedTree = newTree

cache.subTreeData = subTreeData

return {
canonicalUrl: href,
pushRef: { pendingPush, mpaNavigation: false },
cache: cache,
tree: newTree,
}
}

return state
}
29 changes: 26 additions & 3 deletions packages/next/server/dev/hot-reloader.ts
Expand Up @@ -19,7 +19,10 @@ import * as Log from '../../build/output/log'
import getBaseWebpackConfig from '../../build/webpack-config'
import { API_ROUTE, APP_DIR_ALIAS } from '../../lib/constants'
import { recursiveDelete } from '../../lib/recursive-delete'
import { BLOCKED_PAGES } from '../../shared/lib/constants'
import {
BLOCKED_PAGES,
NEXT_CLIENT_SSR_ENTRY_SUFFIX,
} from '../../shared/lib/constants'
import { __ApiPreviewProps } from '../api-utils'
import { getPathMatch } from '../../shared/lib/router/utils/path-match'
import { findPageFile } from '../lib/find-page-file'
Expand Down Expand Up @@ -694,7 +697,12 @@ export default class HotReloader {
(stats: webpack5.Compilation) => {
try {
stats.entrypoints.forEach((entry, key) => {
if (key.startsWith('pages/') || isMiddlewareFilename(key)) {
if (
key.startsWith('pages/') ||
(key.startsWith('app/') &&
!key.endsWith(NEXT_CLIENT_SSR_ENTRY_SUFFIX)) ||
isMiddlewareFilename(key)
) {
// TODO this doesn't handle on demand loaded chunks
entry.chunks.forEach((chunk) => {
if (chunk.id === key) {
Expand Down Expand Up @@ -812,6 +820,12 @@ export default class HotReloader {
changedServerPages,
changedClientPages
)
const serverComponentChanges = serverOnlyChanges.filter((key) =>
key.startsWith('app/')
)
const pageChanges = serverOnlyChanges.filter((key) =>
key.startsWith('pages/')
)
const middlewareChanges = Array.from(changedEdgeServerPages).filter(
(name) => isMiddlewareFilename(name)
)
Expand All @@ -824,14 +838,23 @@ export default class HotReloader {
event: 'middlewareChanges',
})
}
if (serverOnlyChanges.length > 0) {

if (pageChanges.length > 0) {
this.send({
event: 'serverOnlyChanges',
pages: serverOnlyChanges.map((pg) =>
denormalizePagePath(pg.slice('pages'.length))
),
})
}

if (serverComponentChanges.length > 0) {
this.send({
action: 'serverComponentChanges',
// TODO: granular reloading of changes
// entrypoints: serverComponentChanges,
})
}
})

multiCompiler.compilers[0].hooks.failed.tap(
Expand Down
1 change: 1 addition & 0 deletions packages/next/server/dev/on-demand-entry-handler.ts
Expand Up @@ -23,6 +23,7 @@ function treePathToEntrypoint(
): string {
const [parallelRouteKey, segment] = segmentPath

// TODO: modify this path to cover parallelRouteKey convention
const path =
(parentPath ? parentPath + '/' : '') +
(parallelRouteKey !== 'children' ? parallelRouteKey + '/' : '') +
Expand Down
1 change: 1 addition & 0 deletions packages/next/shared/lib/app-router-context.ts
Expand Up @@ -13,6 +13,7 @@ export type CacheNode = {
}

export type AppRouterInstance = {
reload(): void
push(href: string): void
softPush(href: string): void
replace(href: string): void
Expand Down