layout-router.client.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. "client";
  2. "use strict";
  3. Object.defineProperty(exports, "__esModule", {
  4. value: true
  5. });
  6. exports.default = OuterLayoutRouter;
  7. exports.InnerLayoutRouter = InnerLayoutRouter;
  8. var _extends = require("@swc/helpers/lib/_extends.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 _appRouterContext = require("../../shared/lib/app-router-context");
  12. var _appRouterClient = require("./app-router.client");
  13. function OuterLayoutRouter({ parallelRouterKey , segmentPath , childProp , error , loading , template , rootLayoutIncluded }) {
  14. const { childNodes , tree , url } = (0, _react).useContext(_appRouterContext.LayoutRouterContext);
  15. // Get the current parallelRouter cache node
  16. let childNodesForParallelRouter = childNodes.get(parallelRouterKey);
  17. // If the parallel router cache node does not exist yet, create it.
  18. // This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode.
  19. if (!childNodesForParallelRouter) {
  20. childNodes.set(parallelRouterKey, new Map());
  21. childNodesForParallelRouter = childNodes.get(parallelRouterKey);
  22. }
  23. // Get the active segment in the tree
  24. // The reason arrays are used in the data format is that these are transferred from the server to the browser so it's optimized to save bytes.
  25. const treeSegment = tree[1][parallelRouterKey][0];
  26. const childPropSegment = Array.isArray(childProp.segment) ? childProp.segment[1] : childProp.segment;
  27. // If segment is an array it's a dynamic route and we want to read the dynamic route value as the segment to get from the cache.
  28. const currentChildSegment = Array.isArray(treeSegment) ? treeSegment[1] : treeSegment;
  29. /**
  30. * Decides which segments to keep rendering, all segments that are not active will be wrapped in `<Offscreen>`.
  31. */ // TODO-APP: Add handling of `<Offscreen>` when it's available.
  32. const preservedSegments = [
  33. currentChildSegment
  34. ];
  35. return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, preservedSegments.map((preservedSegment)=>{
  36. return(/*
  37. - Error boundary
  38. - Only renders error boundary if error component is provided.
  39. - Rendered for each segment to ensure they have their own error state.
  40. - Loading boundary
  41. - Only renders suspense boundary if loading components is provided.
  42. - Rendered for each segment to ensure they have their own loading state.
  43. - Passed to the router during rendering to ensure it can be immediately rendered when suspending on a Flight fetch.
  44. */ /*#__PURE__*/ _react.default.createElement(_appRouterContext.TemplateContext.Provider, {
  45. key: preservedSegment,
  46. value: /*#__PURE__*/ _react.default.createElement(ErrorBoundary, {
  47. errorComponent: error
  48. }, /*#__PURE__*/ _react.default.createElement(LoadingBoundary, {
  49. loading: loading
  50. }, /*#__PURE__*/ _react.default.createElement(InnerLayoutRouter, {
  51. parallelRouterKey: parallelRouterKey,
  52. url: url,
  53. tree: tree,
  54. childNodes: childNodesForParallelRouter,
  55. childProp: childPropSegment === preservedSegment ? childProp : null,
  56. segmentPath: segmentPath,
  57. path: preservedSegment,
  58. isActive: currentChildSegment === preservedSegment,
  59. rootLayoutIncluded: rootLayoutIncluded
  60. })))
  61. }, template));
  62. }));
  63. }
  64. 'client';
  65. // import { matchSegment } from './match-segments'
  66. /**
  67. * Check if every segment in array a and b matches
  68. */ // function equalSegmentPaths(a: Segment[], b: Segment[]) {
  69. // // Comparing length is a fast path.
  70. // return a.length === b.length && a.every((val, i) => matchSegment(val, b[i]))
  71. // }
  72. /**
  73. * Check if flightDataPath matches layoutSegmentPath
  74. */ // function segmentPathMatches(
  75. // flightDataPath: FlightDataPath,
  76. // layoutSegmentPath: FlightSegmentPath
  77. // ): boolean {
  78. // // The last three items are the current segment, tree, and subTreeData
  79. // const pathToLayout = flightDataPath.slice(0, -3)
  80. // return equalSegmentPaths(layoutSegmentPath, pathToLayout)
  81. // }
  82. /**
  83. * Add refetch marker to router state at the point of the current layout segment.
  84. * This ensures the response returned is not further down than the current layout segment.
  85. */ function walkAddRefetch(segmentPathToWalk, treeToRecreate) {
  86. if (segmentPathToWalk) {
  87. const [segment, parallelRouteKey] = segmentPathToWalk;
  88. const isLast = segmentPathToWalk.length === 2;
  89. if (treeToRecreate[0] === segment) {
  90. if (treeToRecreate[1].hasOwnProperty(parallelRouteKey)) {
  91. if (isLast) {
  92. const subTree = walkAddRefetch(undefined, treeToRecreate[1][parallelRouteKey]);
  93. return [
  94. treeToRecreate[0],
  95. _extends({}, treeToRecreate[1], {
  96. [parallelRouteKey]: [
  97. subTree[0],
  98. subTree[1],
  99. subTree[2],
  100. 'refetch',
  101. ]
  102. }),
  103. ];
  104. }
  105. return [
  106. treeToRecreate[0],
  107. _extends({}, treeToRecreate[1], {
  108. [parallelRouteKey]: walkAddRefetch(segmentPathToWalk.slice(2), treeToRecreate[1][parallelRouteKey])
  109. }),
  110. ];
  111. }
  112. }
  113. }
  114. return treeToRecreate;
  115. }
  116. /**
  117. * Used to cache in createInfinitePromise
  118. */ let infinitePromise;
  119. /**
  120. * Create a Promise that does not resolve. This is used to suspend when data is not available yet.
  121. */ function createInfinitePromise() {
  122. if (!infinitePromise) {
  123. // Only create the Promise once
  124. infinitePromise = new Promise(()=>{
  125. // This is used to debug when the rendering is never updated.
  126. // setTimeout(() => {
  127. // infinitePromise = new Error('Infinite promise')
  128. // resolve()
  129. // }, 5000)
  130. });
  131. }
  132. return infinitePromise;
  133. }
  134. /**
  135. * Check if the top of the HTMLElement is in the viewport.
  136. */ function topOfElementInViewport(element) {
  137. const rect = element.getBoundingClientRect();
  138. return rect.top >= 0;
  139. }
  140. function InnerLayoutRouter({ parallelRouterKey , url , childNodes , childProp , segmentPath , tree , // TODO-APP: implement `<Offscreen>` when available.
  141. // isActive,
  142. path , rootLayoutIncluded }) {
  143. const { changeByServerResponse , tree: fullTree , focusAndScrollRef , } = (0, _react).useContext(_appRouterContext.GlobalLayoutRouterContext);
  144. const focusAndScrollElementRef = (0, _react).useRef(null);
  145. (0, _react).useEffect(()=>{
  146. // Handle scroll and focus, it's only applied once in the first useEffect that triggers that changed.
  147. if (focusAndScrollRef.apply && focusAndScrollElementRef.current) {
  148. // State is mutated to ensure that the focus and scroll is applied only once.
  149. focusAndScrollRef.apply = false;
  150. // Set focus on the element
  151. focusAndScrollElementRef.current.focus();
  152. // Only scroll into viewport when the layout is not visible currently.
  153. if (!topOfElementInViewport(focusAndScrollElementRef.current)) {
  154. const htmlElement = document.documentElement;
  155. const existing = htmlElement.style.scrollBehavior;
  156. htmlElement.style.scrollBehavior = 'auto';
  157. focusAndScrollElementRef.current.scrollIntoView();
  158. htmlElement.style.scrollBehavior = existing;
  159. }
  160. }
  161. }, [
  162. focusAndScrollRef
  163. ]);
  164. // Read segment path from the parallel router cache node.
  165. let childNode = childNodes.get(path);
  166. // If childProp is available this means it's the Flight / SSR case.
  167. if (childProp && // TODO-APP: verify if this can be null based on user code
  168. childProp.current !== null && !childNode /*&&
  169. !childProp.partial*/ ) {
  170. // Add the segment's subTreeData to the cache.
  171. // This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode.
  172. childNodes.set(path, {
  173. data: null,
  174. subTreeData: childProp.current,
  175. parallelRoutes: new Map()
  176. });
  177. // Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache.
  178. childProp.current = null;
  179. // In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again.
  180. childNode = childNodes.get(path);
  181. }
  182. // When childNode is not available during rendering client-side we need to fetch it from the server.
  183. if (!childNode) {
  184. /**
  185. * Router state with refetch marker added
  186. */ // TODO-APP: remove ''
  187. const refetchTree = walkAddRefetch([
  188. '',
  189. ...segmentPath
  190. ], fullTree);
  191. /**
  192. * Flight data fetch kicked off during render and put into the cache.
  193. */ childNodes.set(path, {
  194. data: (0, _appRouterClient).fetchServerResponse(new URL(url, location.origin), refetchTree),
  195. subTreeData: null,
  196. parallelRoutes: new Map()
  197. });
  198. // In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again.
  199. childNode = childNodes.get(path);
  200. }
  201. // This case should never happen so it throws an error. It indicates there's a bug in the Next.js.
  202. if (!childNode) {
  203. throw new Error('Child node should always exist');
  204. }
  205. // This case should never happen so it throws an error. It indicates there's a bug in the Next.js.
  206. if (childNode.subTreeData && childNode.data) {
  207. throw new Error('Child node should not have both subTreeData and data');
  208. }
  209. // If cache node has a data request we have to unwrap response by `use` and update the cache.
  210. if (childNode.data) {
  211. // TODO-APP: error case
  212. /**
  213. * Flight response data
  214. */ // When the data has not resolved yet `use` will suspend here.
  215. const [flightData] = (0, _react).experimental_use(childNode.data);
  216. // Handle case when navigating to page in `pages` from `app`
  217. if (typeof flightData === 'string') {
  218. window.location.href = url;
  219. return null;
  220. }
  221. /**
  222. * If the fast path was triggered.
  223. * The fast path is when the returned Flight data path matches the layout segment path, then we can write the data to the cache in render instead of dispatching an action.
  224. */ let fastPath = false;
  225. // If there are multiple patches returned in the Flight data we need to dispatch to ensure a single render.
  226. // if (flightData.length === 1) {
  227. // const flightDataPath = flightData[0]
  228. // if (segmentPathMatches(flightDataPath, segmentPath)) {
  229. // // Ensure data is set to null as subTreeData will be set in the cache now.
  230. // childNode.data = null
  231. // // Last item is the subtreeData
  232. // // TODO-APP: routerTreePatch needs to be applied to the tree, handle it in render?
  233. // const [, /* routerTreePatch */ subTreeData] = flightDataPath.slice(-2)
  234. // // Add subTreeData into the cache
  235. // childNode.subTreeData = subTreeData
  236. // // This field is required for new items
  237. // childNode.parallelRoutes = new Map()
  238. // fastPath = true
  239. // }
  240. // }
  241. // When the fast path is not used a new action is dispatched to update the tree and cache.
  242. if (!fastPath) {
  243. // segmentPath from the server does not match the layout's segmentPath
  244. childNode.data = null;
  245. // setTimeout is used to start a new transition during render, this is an intentional hack around React.
  246. setTimeout(()=>{
  247. // @ts-ignore startTransition exists
  248. _react.default.startTransition(()=>{
  249. // TODO-APP: handle redirect
  250. changeByServerResponse(fullTree, flightData);
  251. });
  252. });
  253. // Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered.
  254. throw createInfinitePromise();
  255. }
  256. }
  257. // If cache node has no subTreeData and no data request we have to infinitely suspend as the data will likely flow in from another place.
  258. // TODO-APP: double check users can't return null in a component that will kick in here.
  259. if (!childNode.subTreeData) {
  260. throw createInfinitePromise();
  261. }
  262. const subtree = // The layout router context narrows down tree and childNodes at each level.
  263. /*#__PURE__*/ _react.default.createElement(_appRouterContext.LayoutRouterContext.Provider, {
  264. value: {
  265. tree: tree[1][parallelRouterKey],
  266. childNodes: childNode.parallelRoutes,
  267. // TODO-APP: overriding of url for parallel routes
  268. url: url
  269. }
  270. }, childNode.subTreeData);
  271. // Ensure root layout is not wrapped in a div as the root layout renders `<html>`
  272. return rootLayoutIncluded ? /*#__PURE__*/ _react.default.createElement("div", {
  273. ref: focusAndScrollElementRef
  274. }, subtree) : subtree;
  275. }
  276. /**
  277. * Renders suspense boundary with the provided "loading" property as the fallback.
  278. * If no loading property is provided it renders the children without a suspense boundary.
  279. */ function LoadingBoundary({ children , loading }) {
  280. if (loading) {
  281. return /*#__PURE__*/ _react.default.createElement(_react.default.Suspense, {
  282. fallback: loading
  283. }, children);
  284. }
  285. return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, children);
  286. }
  287. /**
  288. * Handles errors through `getDerivedStateFromError`.
  289. * Renders the provided error component and provides a way to `reset` the error boundary state.
  290. */ class ErrorBoundaryHandler extends _react.default.Component {
  291. static getDerivedStateFromError(error) {
  292. return {
  293. error
  294. };
  295. }
  296. render() {
  297. if (this.state.error) {
  298. return /*#__PURE__*/ _react.default.createElement(this.props.errorComponent, {
  299. error: this.state.error,
  300. reset: this.reset
  301. });
  302. }
  303. return this.props.children;
  304. }
  305. constructor(props){
  306. super(props);
  307. this.reset = ()=>{
  308. this.setState({
  309. error: null
  310. });
  311. };
  312. this.state = {
  313. error: null
  314. };
  315. }
  316. }
  317. /**
  318. * Renders error boundary with the provided "errorComponent" property as the fallback.
  319. * If no "errorComponent" property is provided it renders the children without an error boundary.
  320. */ function ErrorBoundary({ errorComponent , children }) {
  321. if (errorComponent) {
  322. return /*#__PURE__*/ _react.default.createElement(ErrorBoundaryHandler, {
  323. errorComponent: errorComponent
  324. }, children);
  325. }
  326. return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, children);
  327. }
  328. if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
  329. Object.defineProperty(exports.default, '__esModule', { value: true });
  330. Object.assign(exports.default, exports);
  331. module.exports = exports.default;
  332. }
  333. //# sourceMappingURL=layout-router.client.js.map