Skip to content

Commit

Permalink
Add refreshing of Server Components (#38508)
Browse files Browse the repository at this point in the history
* Add todo

* Reload page when server component changes

* Implement router.reload() that refreshes full tree
  • Loading branch information
timneutkens committed Jul 11, 2022
1 parent 7e47b86 commit 419765a
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 25 deletions.
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

0 comments on commit 419765a

Please sign in to comment.