UNPKG

next

Version:

The React Framework

266 lines (264 loc) • 11.6 kB
"use strict"; 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