next
Version:
The React Framework
266 lines (264 loc) • 11.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = AppRouter;
exports.fetchServerResponse = fetchServerResponse;
var _interop_require_wildcard = require("@swc/helpers/lib/_interop_require_wildcard.js").default;
var _react = _interop_require_wildcard(require("react"));
var _reactServerDomWebpack = require("next/dist/compiled/react-server-dom-webpack");
var _appRouterContext = require("../../shared/lib/app-router-context");
var _reducer = require("./reducer");
var _hooksClientContext = require("./hooks-client-context");
function AppRouter({ initialTree , initialCanonicalUrl , initialStylesheets , children , hotReloader }) {
const [{ tree , cache , pushRef , focusAndScrollRef , canonicalUrl }, dispatch] = _react.default.useReducer(_reducer.reducer, {
tree: initialTree,
cache: {
data: null,
subTreeData: children,
parallelRoutes: typeof window === 'undefined' ? new Map() : initialParallelRoutes
},
pushRef: {
pendingPush: false,
mpaNavigation: false
},
focusAndScrollRef: {
apply: false
},
canonicalUrl: initialCanonicalUrl + // Hash is read as the initial value for canonicalUrl in the browser
// This is safe to do as canonicalUrl can't be rendered, it's only used to control the history updates the useEffect further down.
(typeof window !== 'undefined' ? window.location.hash : '')
});
(0, _react).useEffect(()=>{
// Ensure initialParallelRoutes is cleaned up from memory once it's used.
initialParallelRoutes = null;
}, []);
// Add memoized pathname/query for useSearchParams and usePathname.
const { searchParams , pathname } = _react.default.useMemo(()=>{
const url = new URL(canonicalUrl, typeof window === 'undefined' ? 'http://n' : window.location.href);
// Convert searchParams to a plain object to match server-side.
const searchParamsObj = {};
url.searchParams.forEach((value, key)=>{
searchParamsObj[key] = value;
});
return {
searchParams: searchParamsObj,
pathname: url.pathname
};
}, [
canonicalUrl
]);
/**
* Server response that only patches the cache and tree.
*/ const changeByServerResponse = _react.default.useCallback((previousTree, flightData)=>{
dispatch({
type: _reducer.ACTION_SERVER_PATCH,
flightData,
previousTree,
cache: {
data: null,
subTreeData: null,
parallelRoutes: new Map()
}
});
}, []);
/**
* The app router that is exposed through `useRouter`. It's only concerned with dispatching actions to the reducer, does not hold state.
*/ const appRouter = _react.default.useMemo(()=>{
const navigate = (href, cacheType, navigateType)=>{
return dispatch({
type: _reducer.ACTION_NAVIGATE,
url: new URL(href, location.origin),
cacheType,
navigateType,
cache: {
data: null,
subTreeData: null,
parallelRoutes: new Map()
},
mutable: {}
});
};
const routerInstance = {
// TODO-APP: implement prefetching of flight
prefetch: (_href)=>Promise.resolve(),
replace: (href)=>{
// @ts-ignore startTransition exists
_react.default.startTransition(()=>{
navigate(href, 'hard', 'replace');
});
},
softReplace: (href)=>{
// @ts-ignore startTransition exists
_react.default.startTransition(()=>{
navigate(href, 'soft', 'replace');
});
},
softPush: (href)=>{
// @ts-ignore startTransition exists
_react.default.startTransition(()=>{
navigate(href, 'soft', 'push');
});
},
push: (href)=>{
// @ts-ignore startTransition exists
_react.default.startTransition(()=>{
navigate(href, 'hard', 'push');
});
},
reload: ()=>{
// @ts-ignore startTransition exists
_react.default.startTransition(()=>{
dispatch({
type: _reducer.ACTION_RELOAD,
// TODO-APP: revisit if this needs to be passed.
url: new URL(window.location.href),
cache: {
data: null,
subTreeData: null,
parallelRoutes: new Map()
},
mutable: {}
});
});
}
};
return routerInstance;
}, []);
(0, _react).useEffect(()=>{
// When mpaNavigation flag is set do a hard navigation to the new url.
if (pushRef.mpaNavigation) {
window.location.href = canonicalUrl;
return;
}
// Identifier is shortened intentionally.
// __NA is used to identify if the history entry can be handled by the app-router.
// __N is used to identify if the history entry can be handled by the old router.
const historyState = {
__NA: true,
tree
};
if (pushRef.pendingPush) {
// This intentionally mutates React state, pushRef is overwritten to ensure additional push/replace calls do not trigger an additional history entry.
pushRef.pendingPush = false;
window.history.pushState(historyState, '', canonicalUrl);
} else {
window.history.replaceState(historyState, '', canonicalUrl);
}
}, [
tree,
pushRef,
canonicalUrl
]);
// Add `window.nd` for debugging purposes.
// This is not meant for use in applications as concurrent rendering will affect the cache/tree/router.
if (typeof window !== 'undefined') {
// @ts-ignore this is for debugging
window.nd = {
router: appRouter,
cache,
tree
};
}
/**
* Handle popstate event, this is used to handle back/forward in the browser.
* By default dispatches ACTION_RESTORE, however if the history entry was not pushed/replaced by app-router it will reload the page.
* That case can happen when the old router injected the history entry.
*/ const onPopState = _react.default.useCallback(({ state })=>{
if (!state) {
// TODO-APP: this case only happens when pushState/replaceState was called outside of Next.js. It should probably reload the page in this case.
return;
}
// TODO-APP: this case happens when pushState/replaceState was called outside of Next.js or when the history entry was pushed by the old router.
// It reloads the page in this case but we might have to revisit this as the old router ignores it.
if (!state.__NA) {
window.location.reload();
return;
}
// @ts-ignore useTransition exists
// TODO-APP: Ideally the back button should not use startTransition as it should apply the updates synchronously
// Without startTransition works if the cache is there for this path
_react.default.startTransition(()=>{
dispatch({
type: _reducer.ACTION_RESTORE,
url: new URL(window.location.href),
tree: state.tree
});
});
}, []);
// Register popstate event to call onPopstate.
_react.default.useEffect(()=>{
window.addEventListener('popstate', onPopState);
return ()=>{
window.removeEventListener('popstate', onPopState);
};
}, [
onPopState
]);
return /*#__PURE__*/ _react.default.createElement(_hooksClientContext.PathnameContext.Provider, {
value: pathname
}, /*#__PURE__*/ _react.default.createElement(_hooksClientContext.SearchParamsContext.Provider, {
value: searchParams
}, /*#__PURE__*/ _react.default.createElement(_appRouterContext.GlobalLayoutRouterContext.Provider, {
value: {
changeByServerResponse,
tree,
focusAndScrollRef
}
}, /*#__PURE__*/ _react.default.createElement(_appRouterContext.AppRouterContext.Provider, {
value: appRouter
}, /*#__PURE__*/ _react.default.createElement(_appRouterContext.LayoutRouterContext.Provider, {
value: {
childNodes: cache.parallelRoutes,
tree: tree,
// Root node always has `url`
// Provided in AppTreeContext to ensure it can be overwritten in layout-router
url: canonicalUrl,
stylesheets: initialStylesheets
}
}, /*#__PURE__*/ _react.default.createElement(ErrorOverlay, null, // ErrorOverlay intentionally only wraps the children of app-router.
cache.subTreeData), // HotReloader uses the router tree and router.reload() in order to apply Server Component changes.
hotReloader)))));
}
/**
* Fetch the flight data for the provided url. Takes in the current router state to decide what to render server-side.
*/ function fetchFlight(url, flightRouterState) {
const flightUrl = new URL(url);
const searchParams = flightUrl.searchParams;
// Enable flight response
searchParams.append('__flight__', '1');
// Provide the current router state
searchParams.append('__flight_router_state_tree__', JSON.stringify(flightRouterState));
// TODO-APP: Verify that TransformStream is supported.
const { readable , writable } = new TransformStream();
fetch(flightUrl.toString()).then((res)=>{
var ref;
(ref = res.body) == null ? void 0 : ref.pipeTo(writable);
});
return readable;
}
function fetchServerResponse(url, flightRouterState) {
// Handle the `fetch` readable stream that can be read using `readRoot`.
return (0, _reactServerDomWebpack).createFromReadableStream(fetchFlight(url, flightRouterState));
}
/**
* Renders development error overlay when NODE_ENV is development.
*/ function ErrorOverlay({ children }) {
if (process.env.NODE_ENV === 'production') {
return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, children);
} else {
const { ReactDevOverlay , } = require('next/dist/compiled/@next/react-dev-overlay/dist/client');
return /*#__PURE__*/ _react.default.createElement(ReactDevOverlay, {
globalOverlay: true
}, children);
}
}
// Ensure the initialParallelRoutes are not combined because of double-rendering in the browser with Strict Mode.
// TODO-APP: move this back into AppRouter
let initialParallelRoutes = typeof window === 'undefined' ? null : new Map();
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=app-router.client.js.map
;