app-router.client.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. "client";
  2. "use strict";
  3. Object.defineProperty(exports, "__esModule", {
  4. value: true
  5. });
  6. exports.default = AppRouter;
  7. exports.fetchServerResponse = fetchServerResponse;
  8. var _async_to_generator = require("@swc/helpers/lib/_async_to_generator.js").default;
  9. var _interop_require_wildcard = require("@swc/helpers/lib/_interop_require_wildcard.js").default;
  10. var _react = _interop_require_wildcard(require("react"));
  11. var _reactServerDomWebpack = require("next/dist/compiled/react-server-dom-webpack");
  12. var _appRouterContext = require("../../shared/lib/app-router-context");
  13. var _reducer = require("./reducer");
  14. var _hooksClientContext = require("./hooks-client-context");
  15. var _useReducerWithDevtools = require("./use-reducer-with-devtools");
  16. function AppRouter({ initialTree , initialCanonicalUrl , children , hotReloader }) {
  17. const initialState = (0, _react).useMemo(()=>{
  18. return {
  19. tree: initialTree,
  20. cache: {
  21. data: null,
  22. subTreeData: children,
  23. parallelRoutes: typeof window === 'undefined' ? new Map() : initialParallelRoutes
  24. },
  25. prefetchCache: new Map(),
  26. pushRef: {
  27. pendingPush: false,
  28. mpaNavigation: false
  29. },
  30. focusAndScrollRef: {
  31. apply: false
  32. },
  33. canonicalUrl: initialCanonicalUrl + // Hash is read as the initial value for canonicalUrl in the browser
  34. // This is safe to do as canonicalUrl can't be rendered, it's only used to control the history updates the useEffect further down.
  35. (typeof window !== 'undefined' ? window.location.hash : '')
  36. };
  37. }, [
  38. children,
  39. initialCanonicalUrl,
  40. initialTree
  41. ]);
  42. const [{ tree , cache , prefetchCache , pushRef , focusAndScrollRef , canonicalUrl }, dispatch, sync, ] = (0, _useReducerWithDevtools).useReducerWithReduxDevtools(_reducer.reducer, initialState);
  43. (0, _react).useEffect(()=>{
  44. // Ensure initialParallelRoutes is cleaned up from memory once it's used.
  45. initialParallelRoutes = null;
  46. }, []);
  47. // Add memoized pathname/query for useSearchParams and usePathname.
  48. const { searchParams , pathname } = (0, _react).useMemo(()=>{
  49. const url = new URL(canonicalUrl, typeof window === 'undefined' ? 'http://n' : window.location.href);
  50. // Convert searchParams to a plain object to match server-side.
  51. const searchParamsObj = {};
  52. url.searchParams.forEach((value, key)=>{
  53. searchParamsObj[key] = value;
  54. });
  55. return {
  56. searchParams: searchParamsObj,
  57. pathname: url.pathname
  58. };
  59. }, [
  60. canonicalUrl
  61. ]);
  62. /**
  63. * Server response that only patches the cache and tree.
  64. */ const changeByServerResponse = (0, _react).useCallback((previousTree, flightData)=>{
  65. dispatch({
  66. type: _reducer.ACTION_SERVER_PATCH,
  67. flightData,
  68. previousTree,
  69. cache: {
  70. data: null,
  71. subTreeData: null,
  72. parallelRoutes: new Map()
  73. },
  74. mutable: {}
  75. });
  76. }, [
  77. dispatch
  78. ]);
  79. /**
  80. * The app router that is exposed through `useRouter`. It's only concerned with dispatching actions to the reducer, does not hold state.
  81. */ const appRouter = (0, _react).useMemo(()=>{
  82. const navigate = (href, navigateType, forceOptimisticNavigation)=>{
  83. return dispatch({
  84. type: _reducer.ACTION_NAVIGATE,
  85. url: new URL(href, location.origin),
  86. forceOptimisticNavigation,
  87. navigateType,
  88. cache: {
  89. data: null,
  90. subTreeData: null,
  91. parallelRoutes: new Map()
  92. },
  93. mutable: {}
  94. });
  95. };
  96. const routerInstance = {
  97. // TODO-APP: implement prefetching of flight
  98. prefetch: _async_to_generator(function*(href) {
  99. // If prefetch has already been triggered, don't trigger it again.
  100. if (prefetched.has(href)) {
  101. return;
  102. }
  103. prefetched.add(href);
  104. const url = new URL(href, location.origin);
  105. try {
  106. var // initialTree is used when history.state.tree is missing because the history state is set in `useEffect` below, it being missing means this is the hydration case.
  107. ref;
  108. // TODO-APP: handle case where history.state is not the new router history entry
  109. const r = fetchServerResponse(url, ((ref = window.history.state) == null ? void 0 : ref.tree) || initialTree, true);
  110. const [flightData] = yield r;
  111. // @ts-ignore startTransition exists
  112. _react.default.startTransition(()=>{
  113. dispatch({
  114. type: _reducer.ACTION_PREFETCH,
  115. url,
  116. flightData
  117. });
  118. });
  119. } catch (err) {
  120. console.error('PREFETCH ERROR', err);
  121. }
  122. }),
  123. replace: (href, options = {})=>{
  124. // @ts-ignore startTransition exists
  125. _react.default.startTransition(()=>{
  126. navigate(href, 'replace', Boolean(options.forceOptimisticNavigation));
  127. });
  128. },
  129. push: (href, options = {})=>{
  130. // @ts-ignore startTransition exists
  131. _react.default.startTransition(()=>{
  132. navigate(href, 'push', Boolean(options.forceOptimisticNavigation));
  133. });
  134. },
  135. reload: ()=>{
  136. // @ts-ignore startTransition exists
  137. _react.default.startTransition(()=>{
  138. dispatch({
  139. type: _reducer.ACTION_RELOAD,
  140. // TODO-APP: revisit if this needs to be passed.
  141. cache: {
  142. data: null,
  143. subTreeData: null,
  144. parallelRoutes: new Map()
  145. },
  146. mutable: {}
  147. });
  148. });
  149. }
  150. };
  151. return routerInstance;
  152. }, [
  153. dispatch,
  154. initialTree
  155. ]);
  156. (0, _react).useEffect(()=>{
  157. // When mpaNavigation flag is set do a hard navigation to the new url.
  158. if (pushRef.mpaNavigation) {
  159. window.location.href = canonicalUrl;
  160. return;
  161. }
  162. // Identifier is shortened intentionally.
  163. // __NA is used to identify if the history entry can be handled by the app-router.
  164. // __N is used to identify if the history entry can be handled by the old router.
  165. const historyState = {
  166. __NA: true,
  167. tree
  168. };
  169. if (pushRef.pendingPush) {
  170. // This intentionally mutates React state, pushRef is overwritten to ensure additional push/replace calls do not trigger an additional history entry.
  171. pushRef.pendingPush = false;
  172. window.history.pushState(historyState, '', canonicalUrl);
  173. } else {
  174. window.history.replaceState(historyState, '', canonicalUrl);
  175. }
  176. sync();
  177. }, [
  178. tree,
  179. pushRef,
  180. canonicalUrl,
  181. sync
  182. ]);
  183. // Add `window.nd` for debugging purposes.
  184. // This is not meant for use in applications as concurrent rendering will affect the cache/tree/router.
  185. if (typeof window !== 'undefined') {
  186. // @ts-ignore this is for debugging
  187. window.nd = {
  188. router: appRouter,
  189. cache,
  190. prefetchCache,
  191. tree
  192. };
  193. }
  194. /**
  195. * Handle popstate event, this is used to handle back/forward in the browser.
  196. * By default dispatches ACTION_RESTORE, however if the history entry was not pushed/replaced by app-router it will reload the page.
  197. * That case can happen when the old router injected the history entry.
  198. */ const onPopState = (0, _react).useCallback(({ state })=>{
  199. if (!state) {
  200. // TODO-APP: this case only happens when pushState/replaceState was called outside of Next.js. It should probably reload the page in this case.
  201. return;
  202. }
  203. // 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.
  204. // It reloads the page in this case but we might have to revisit this as the old router ignores it.
  205. if (!state.__NA) {
  206. window.location.reload();
  207. return;
  208. }
  209. // @ts-ignore useTransition exists
  210. // TODO-APP: Ideally the back button should not use startTransition as it should apply the updates synchronously
  211. // Without startTransition works if the cache is there for this path
  212. _react.default.startTransition(()=>{
  213. dispatch({
  214. type: _reducer.ACTION_RESTORE,
  215. url: new URL(window.location.href),
  216. tree: state.tree
  217. });
  218. });
  219. }, [
  220. dispatch
  221. ]);
  222. // Register popstate event to call onPopstate.
  223. (0, _react).useEffect(()=>{
  224. window.addEventListener('popstate', onPopState);
  225. return ()=>{
  226. window.removeEventListener('popstate', onPopState);
  227. };
  228. }, [
  229. onPopState
  230. ]);
  231. return /*#__PURE__*/ _react.default.createElement(_hooksClientContext.PathnameContext.Provider, {
  232. value: pathname
  233. }, /*#__PURE__*/ _react.default.createElement(_hooksClientContext.SearchParamsContext.Provider, {
  234. value: searchParams
  235. }, /*#__PURE__*/ _react.default.createElement(_appRouterContext.GlobalLayoutRouterContext.Provider, {
  236. value: {
  237. changeByServerResponse,
  238. tree,
  239. focusAndScrollRef
  240. }
  241. }, /*#__PURE__*/ _react.default.createElement(_appRouterContext.AppRouterContext.Provider, {
  242. value: appRouter
  243. }, /*#__PURE__*/ _react.default.createElement(_appRouterContext.LayoutRouterContext.Provider, {
  244. value: {
  245. childNodes: cache.parallelRoutes,
  246. tree: tree,
  247. // Root node always has `url`
  248. // Provided in AppTreeContext to ensure it can be overwritten in layout-router
  249. url: canonicalUrl
  250. }
  251. }, /*#__PURE__*/ _react.default.createElement(ErrorOverlay, null, // ErrorOverlay intentionally only wraps the children of app-router.
  252. cache.subTreeData), // HotReloader uses the router tree and router.reload() in order to apply Server Component changes.
  253. hotReloader)))));
  254. }
  255. 'client';
  256. function fetchServerResponse(url, flightRouterState, prefetch) {
  257. return _fetchServerResponse.apply(this, arguments);
  258. }
  259. function _fetchServerResponse() {
  260. _fetchServerResponse = _async_to_generator(function*(url, flightRouterState, prefetch) {
  261. const flightUrl = new URL(url);
  262. const searchParams = flightUrl.searchParams;
  263. // Enable flight response
  264. searchParams.append('__flight__', '1');
  265. // Provide the current router state
  266. searchParams.append('__flight_router_state_tree__', JSON.stringify(flightRouterState));
  267. if (prefetch) {
  268. searchParams.append('__flight_prefetch__', '1');
  269. }
  270. const res = yield fetch(flightUrl.toString());
  271. // Handle the `fetch` readable stream that can be unwrapped by `React.use`.
  272. const flightData = yield (0, _reactServerDomWebpack).createFromFetch(Promise.resolve(res));
  273. return [
  274. flightData
  275. ];
  276. });
  277. return _fetchServerResponse.apply(this, arguments);
  278. }
  279. /**
  280. * Renders development error overlay when NODE_ENV is development.
  281. */ function ErrorOverlay({ children }) {
  282. if (process.env.NODE_ENV === 'production') {
  283. return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, children);
  284. } else {
  285. const { ReactDevOverlay , } = require('next/dist/compiled/@next/react-dev-overlay/dist/client');
  286. return /*#__PURE__*/ _react.default.createElement(ReactDevOverlay, {
  287. globalOverlay: true
  288. }, children);
  289. }
  290. }
  291. // Ensure the initialParallelRoutes are not combined because of double-rendering in the browser with Strict Mode.
  292. // TODO-APP: move this back into AppRouter
  293. let initialParallelRoutes = typeof window === 'undefined' ? null : new Map();
  294. const prefetched = new Set();
  295. if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
  296. Object.defineProperty(exports.default, '__esModule', { value: true });
  297. Object.assign(exports.default, exports);
  298. module.exports = exports.default;
  299. }
  300. //# sourceMappingURL=app-router.client.js.map