router.ts 111 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682
  1. import type { History, Location, Path, To } from "./history";
  2. import {
  3. Action as HistoryAction,
  4. createLocation,
  5. createPath,
  6. invariant,
  7. parsePath,
  8. } from "./history";
  9. import type {
  10. DataResult,
  11. AgnosticDataRouteMatch,
  12. AgnosticDataRouteObject,
  13. DeferredResult,
  14. ErrorResult,
  15. FormEncType,
  16. FormMethod,
  17. RedirectResult,
  18. RouteData,
  19. AgnosticRouteObject,
  20. Submission,
  21. SuccessResult,
  22. AgnosticRouteMatch,
  23. MutationFormMethod,
  24. ShouldRevalidateFunction,
  25. } from "./utils";
  26. import {
  27. DeferredData,
  28. ErrorResponse,
  29. ResultType,
  30. convertRoutesToDataRoutes,
  31. getPathContributingMatches,
  32. isRouteErrorResponse,
  33. joinPaths,
  34. matchRoutes,
  35. resolveTo,
  36. warning,
  37. } from "./utils";
  38. ////////////////////////////////////////////////////////////////////////////////
  39. //#region Types and Constants
  40. ////////////////////////////////////////////////////////////////////////////////
  41. /**
  42. * A Router instance manages all navigation and data loading/mutations
  43. */
  44. export interface Router {
  45. /**
  46. * @internal
  47. * PRIVATE - DO NOT USE
  48. *
  49. * Return the basename for the router
  50. */
  51. get basename(): RouterInit["basename"];
  52. /**
  53. * @internal
  54. * PRIVATE - DO NOT USE
  55. *
  56. * Return the current state of the router
  57. */
  58. get state(): RouterState;
  59. /**
  60. * @internal
  61. * PRIVATE - DO NOT USE
  62. *
  63. * Return the routes for this router instance
  64. */
  65. get routes(): AgnosticDataRouteObject[];
  66. /**
  67. * @internal
  68. * PRIVATE - DO NOT USE
  69. *
  70. * Initialize the router, including adding history listeners and kicking off
  71. * initial data fetches. Returns a function to cleanup listeners and abort
  72. * any in-progress loads
  73. */
  74. initialize(): Router;
  75. /**
  76. * @internal
  77. * PRIVATE - DO NOT USE
  78. *
  79. * Subscribe to router.state updates
  80. *
  81. * @param fn function to call with the new state
  82. */
  83. subscribe(fn: RouterSubscriber): () => void;
  84. /**
  85. * @internal
  86. * PRIVATE - DO NOT USE
  87. *
  88. * Enable scroll restoration behavior in the router
  89. *
  90. * @param savedScrollPositions Object that will manage positions, in case
  91. * it's being restored from sessionStorage
  92. * @param getScrollPosition Function to get the active Y scroll position
  93. * @param getKey Function to get the key to use for restoration
  94. */
  95. enableScrollRestoration(
  96. savedScrollPositions: Record<string, number>,
  97. getScrollPosition: GetScrollPositionFunction,
  98. getKey?: GetScrollRestorationKeyFunction
  99. ): () => void;
  100. /**
  101. * @internal
  102. * PRIVATE - DO NOT USE
  103. *
  104. * Navigate forward/backward in the history stack
  105. * @param to Delta to move in the history stack
  106. */
  107. navigate(to: number): Promise<void>;
  108. /**
  109. * Navigate to the given path
  110. * @param to Path to navigate to
  111. * @param opts Navigation options (method, submission, etc.)
  112. */
  113. navigate(to: To, opts?: RouterNavigateOptions): Promise<void>;
  114. /**
  115. * @internal
  116. * PRIVATE - DO NOT USE
  117. *
  118. * Trigger a fetcher load/submission
  119. *
  120. * @param key Fetcher key
  121. * @param routeId Route that owns the fetcher
  122. * @param href href to fetch
  123. * @param opts Fetcher options, (method, submission, etc.)
  124. */
  125. fetch(
  126. key: string,
  127. routeId: string,
  128. href: string,
  129. opts?: RouterNavigateOptions
  130. ): void;
  131. /**
  132. * @internal
  133. * PRIVATE - DO NOT USE
  134. *
  135. * Trigger a revalidation of all current route loaders and fetcher loads
  136. */
  137. revalidate(): void;
  138. /**
  139. * @internal
  140. * PRIVATE - DO NOT USE
  141. *
  142. * Utility function to create an href for the given location
  143. * @param location
  144. */
  145. createHref(location: Location | URL): string;
  146. /**
  147. * @internal
  148. * PRIVATE - DO NOT USE
  149. *
  150. * Utility function to URL encode a destination path according to the internal
  151. * history implementation
  152. * @param to
  153. */
  154. encodeLocation(to: To): Path;
  155. /**
  156. * @internal
  157. * PRIVATE - DO NOT USE
  158. *
  159. * Get/create a fetcher for the given key
  160. * @param key
  161. */
  162. getFetcher<TData = any>(key?: string): Fetcher<TData>;
  163. /**
  164. * @internal
  165. * PRIVATE - DO NOT USE
  166. *
  167. * Delete the fetcher for a given key
  168. * @param key
  169. */
  170. deleteFetcher(key?: string): void;
  171. /**
  172. * @internal
  173. * PRIVATE - DO NOT USE
  174. *
  175. * Cleanup listeners and abort any in-progress loads
  176. */
  177. dispose(): void;
  178. /**
  179. * @internal
  180. * PRIVATE - DO NOT USE
  181. *
  182. * Get a navigation blocker
  183. * @param key The identifier for the blocker
  184. * @param fn The blocker function implementation
  185. */
  186. getBlocker(key: string, fn: BlockerFunction): Blocker;
  187. /**
  188. * @internal
  189. * PRIVATE - DO NOT USE
  190. *
  191. * Delete a navigation blocker
  192. * @param key The identifier for the blocker
  193. */
  194. deleteBlocker(key: string): void;
  195. /**
  196. * @internal
  197. * PRIVATE - DO NOT USE
  198. *
  199. * Internal fetch AbortControllers accessed by unit tests
  200. */
  201. _internalFetchControllers: Map<string, AbortController>;
  202. /**
  203. * @internal
  204. * PRIVATE - DO NOT USE
  205. *
  206. * Internal pending DeferredData instances accessed by unit tests
  207. */
  208. _internalActiveDeferreds: Map<string, DeferredData>;
  209. }
  210. /**
  211. * State maintained internally by the router. During a navigation, all states
  212. * reflect the the "old" location unless otherwise noted.
  213. */
  214. export interface RouterState {
  215. /**
  216. * The action of the most recent navigation
  217. */
  218. historyAction: HistoryAction;
  219. /**
  220. * The current location reflected by the router
  221. */
  222. location: Location;
  223. /**
  224. * The current set of route matches
  225. */
  226. matches: AgnosticDataRouteMatch[];
  227. /**
  228. * Tracks whether we've completed our initial data load
  229. */
  230. initialized: boolean;
  231. /**
  232. * Current scroll position we should start at for a new view
  233. * - number -> scroll position to restore to
  234. * - false -> do not restore scroll at all (used during submissions)
  235. * - null -> don't have a saved position, scroll to hash or top of page
  236. */
  237. restoreScrollPosition: number | false | null;
  238. /**
  239. * Indicate whether this navigation should skip resetting the scroll position
  240. * if we are unable to restore the scroll position
  241. */
  242. preventScrollReset: boolean;
  243. /**
  244. * Tracks the state of the current navigation
  245. */
  246. navigation: Navigation;
  247. /**
  248. * Tracks any in-progress revalidations
  249. */
  250. revalidation: RevalidationState;
  251. /**
  252. * Data from the loaders for the current matches
  253. */
  254. loaderData: RouteData;
  255. /**
  256. * Data from the action for the current matches
  257. */
  258. actionData: RouteData | null;
  259. /**
  260. * Errors caught from loaders for the current matches
  261. */
  262. errors: RouteData | null;
  263. /**
  264. * Map of current fetchers
  265. */
  266. fetchers: Map<string, Fetcher>;
  267. /**
  268. * Map of current blockers
  269. */
  270. blockers: Map<string, Blocker>;
  271. }
  272. /**
  273. * Data that can be passed into hydrate a Router from SSR
  274. */
  275. export type HydrationState = Partial<
  276. Pick<RouterState, "loaderData" | "actionData" | "errors">
  277. >;
  278. /**
  279. * Initialization options for createRouter
  280. */
  281. export interface RouterInit {
  282. basename?: string;
  283. routes: AgnosticRouteObject[];
  284. history: History;
  285. hydrationData?: HydrationState;
  286. }
  287. /**
  288. * State returned from a server-side query() call
  289. */
  290. export interface StaticHandlerContext {
  291. basename: Router["basename"];
  292. location: RouterState["location"];
  293. matches: RouterState["matches"];
  294. loaderData: RouterState["loaderData"];
  295. actionData: RouterState["actionData"];
  296. errors: RouterState["errors"];
  297. statusCode: number;
  298. loaderHeaders: Record<string, Headers>;
  299. actionHeaders: Record<string, Headers>;
  300. activeDeferreds: Record<string, DeferredData> | null;
  301. _deepestRenderedBoundaryId?: string | null;
  302. }
  303. /**
  304. * A StaticHandler instance manages a singular SSR navigation/fetch event
  305. */
  306. export interface StaticHandler {
  307. dataRoutes: AgnosticDataRouteObject[];
  308. query(
  309. request: Request,
  310. opts?: { requestContext?: unknown }
  311. ): Promise<StaticHandlerContext | Response>;
  312. queryRoute(
  313. request: Request,
  314. opts?: { routeId?: string; requestContext?: unknown }
  315. ): Promise<any>;
  316. }
  317. /**
  318. * Subscriber function signature for changes to router state
  319. */
  320. export interface RouterSubscriber {
  321. (state: RouterState): void;
  322. }
  323. interface UseMatchesMatch {
  324. id: string;
  325. pathname: string;
  326. params: AgnosticRouteMatch["params"];
  327. data: unknown;
  328. handle: unknown;
  329. }
  330. /**
  331. * Function signature for determining the key to be used in scroll restoration
  332. * for a given location
  333. */
  334. export interface GetScrollRestorationKeyFunction {
  335. (location: Location, matches: UseMatchesMatch[]): string | null;
  336. }
  337. /**
  338. * Function signature for determining the current scroll position
  339. */
  340. export interface GetScrollPositionFunction {
  341. (): number;
  342. }
  343. /**
  344. * Options for a navigate() call for a Link navigation
  345. */
  346. type LinkNavigateOptions = {
  347. replace?: boolean;
  348. state?: any;
  349. preventScrollReset?: boolean;
  350. };
  351. /**
  352. * Options for a navigate() call for a Form navigation
  353. */
  354. type SubmissionNavigateOptions = {
  355. replace?: boolean;
  356. state?: any;
  357. preventScrollReset?: boolean;
  358. formMethod?: FormMethod;
  359. formEncType?: FormEncType;
  360. formData: FormData;
  361. };
  362. /**
  363. * Options to pass to navigate() for either a Link or Form navigation
  364. */
  365. export type RouterNavigateOptions =
  366. | LinkNavigateOptions
  367. | SubmissionNavigateOptions;
  368. /**
  369. * Options to pass to fetch()
  370. */
  371. export type RouterFetchOptions =
  372. | Omit<LinkNavigateOptions, "replace">
  373. | Omit<SubmissionNavigateOptions, "replace">;
  374. /**
  375. * Potential states for state.navigation
  376. */
  377. export type NavigationStates = {
  378. Idle: {
  379. state: "idle";
  380. location: undefined;
  381. formMethod: undefined;
  382. formAction: undefined;
  383. formEncType: undefined;
  384. formData: undefined;
  385. };
  386. Loading: {
  387. state: "loading";
  388. location: Location;
  389. formMethod: FormMethod | undefined;
  390. formAction: string | undefined;
  391. formEncType: FormEncType | undefined;
  392. formData: FormData | undefined;
  393. };
  394. Submitting: {
  395. state: "submitting";
  396. location: Location;
  397. formMethod: FormMethod;
  398. formAction: string;
  399. formEncType: FormEncType;
  400. formData: FormData;
  401. };
  402. };
  403. export type Navigation = NavigationStates[keyof NavigationStates];
  404. export type RevalidationState = "idle" | "loading";
  405. /**
  406. * Potential states for fetchers
  407. */
  408. type FetcherStates<TData = any> = {
  409. Idle: {
  410. state: "idle";
  411. formMethod: undefined;
  412. formAction: undefined;
  413. formEncType: undefined;
  414. formData: undefined;
  415. data: TData | undefined;
  416. " _hasFetcherDoneAnything "?: boolean;
  417. };
  418. Loading: {
  419. state: "loading";
  420. formMethod: FormMethod | undefined;
  421. formAction: string | undefined;
  422. formEncType: FormEncType | undefined;
  423. formData: FormData | undefined;
  424. data: TData | undefined;
  425. " _hasFetcherDoneAnything "?: boolean;
  426. };
  427. Submitting: {
  428. state: "submitting";
  429. formMethod: FormMethod;
  430. formAction: string;
  431. formEncType: FormEncType;
  432. formData: FormData;
  433. data: TData | undefined;
  434. " _hasFetcherDoneAnything "?: boolean;
  435. };
  436. };
  437. export type Fetcher<TData = any> =
  438. FetcherStates<TData>[keyof FetcherStates<TData>];
  439. interface BlockerBlocked {
  440. state: "blocked";
  441. reset(): void;
  442. proceed(): void;
  443. location: Location;
  444. }
  445. interface BlockerUnblocked {
  446. state: "unblocked";
  447. reset: undefined;
  448. proceed: undefined;
  449. location: undefined;
  450. }
  451. interface BlockerProceeding {
  452. state: "proceeding";
  453. reset: undefined;
  454. proceed: undefined;
  455. location: Location;
  456. }
  457. export type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;
  458. export type BlockerFunction = (args: {
  459. currentLocation: Location;
  460. nextLocation: Location;
  461. historyAction: HistoryAction;
  462. }) => boolean;
  463. interface ShortCircuitable {
  464. /**
  465. * startNavigation does not need to complete the navigation because we
  466. * redirected or got interrupted
  467. */
  468. shortCircuited?: boolean;
  469. }
  470. interface HandleActionResult extends ShortCircuitable {
  471. /**
  472. * Error thrown from the current action, keyed by the route containing the
  473. * error boundary to render the error. To be committed to the state after
  474. * loaders have completed
  475. */
  476. pendingActionError?: RouteData;
  477. /**
  478. * Data returned from the current action, keyed by the route owning the action.
  479. * To be committed to the state after loaders have completed
  480. */
  481. pendingActionData?: RouteData;
  482. }
  483. interface HandleLoadersResult extends ShortCircuitable {
  484. /**
  485. * loaderData returned from the current set of loaders
  486. */
  487. loaderData?: RouterState["loaderData"];
  488. /**
  489. * errors thrown from the current set of loaders
  490. */
  491. errors?: RouterState["errors"];
  492. }
  493. /**
  494. * Cached info for active fetcher.load() instances so they can participate
  495. * in revalidation
  496. */
  497. interface FetchLoadMatch {
  498. routeId: string;
  499. path: string;
  500. match: AgnosticDataRouteMatch;
  501. matches: AgnosticDataRouteMatch[];
  502. }
  503. /**
  504. * Identified fetcher.load() calls that need to be revalidated
  505. */
  506. interface RevalidatingFetcher extends FetchLoadMatch {
  507. key: string;
  508. }
  509. /**
  510. * Wrapper object to allow us to throw any response out from callLoaderOrAction
  511. * for queryRouter while preserving whether or not it was thrown or returned
  512. * from the loader/action
  513. */
  514. interface QueryRouteResponse {
  515. type: ResultType.data | ResultType.error;
  516. response: Response;
  517. }
  518. const validMutationMethodsArr: MutationFormMethod[] = [
  519. "post",
  520. "put",
  521. "patch",
  522. "delete",
  523. ];
  524. const validMutationMethods = new Set<MutationFormMethod>(
  525. validMutationMethodsArr
  526. );
  527. const validRequestMethodsArr: FormMethod[] = [
  528. "get",
  529. ...validMutationMethodsArr,
  530. ];
  531. const validRequestMethods = new Set<FormMethod>(validRequestMethodsArr);
  532. const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
  533. const redirectPreserveMethodStatusCodes = new Set([307, 308]);
  534. export const IDLE_NAVIGATION: NavigationStates["Idle"] = {
  535. state: "idle",
  536. location: undefined,
  537. formMethod: undefined,
  538. formAction: undefined,
  539. formEncType: undefined,
  540. formData: undefined,
  541. };
  542. export const IDLE_FETCHER: FetcherStates["Idle"] = {
  543. state: "idle",
  544. data: undefined,
  545. formMethod: undefined,
  546. formAction: undefined,
  547. formEncType: undefined,
  548. formData: undefined,
  549. };
  550. export const IDLE_BLOCKER: BlockerUnblocked = {
  551. state: "unblocked",
  552. proceed: undefined,
  553. reset: undefined,
  554. location: undefined,
  555. };
  556. const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
  557. const isBrowser =
  558. typeof window !== "undefined" &&
  559. typeof window.document !== "undefined" &&
  560. typeof window.document.createElement !== "undefined";
  561. const isServer = !isBrowser;
  562. //#endregion
  563. ////////////////////////////////////////////////////////////////////////////////
  564. //#region createRouter
  565. ////////////////////////////////////////////////////////////////////////////////
  566. /**
  567. * Create a router and listen to history POP navigations
  568. */
  569. export function createRouter(init: RouterInit): Router {
  570. invariant(
  571. init.routes.length > 0,
  572. "You must provide a non-empty routes array to createRouter"
  573. );
  574. let dataRoutes = convertRoutesToDataRoutes(init.routes);
  575. // Cleanup function for history
  576. let unlistenHistory: (() => void) | null = null;
  577. // Externally-provided functions to call on all state changes
  578. let subscribers = new Set<RouterSubscriber>();
  579. // Externally-provided object to hold scroll restoration locations during routing
  580. let savedScrollPositions: Record<string, number> | null = null;
  581. // Externally-provided function to get scroll restoration keys
  582. let getScrollRestorationKey: GetScrollRestorationKeyFunction | null = null;
  583. // Externally-provided function to get current scroll position
  584. let getScrollPosition: GetScrollPositionFunction | null = null;
  585. // One-time flag to control the initial hydration scroll restoration. Because
  586. // we don't get the saved positions from <ScrollRestoration /> until _after_
  587. // the initial render, we need to manually trigger a separate updateState to
  588. // send along the restoreScrollPosition
  589. // Set to true if we have `hydrationData` since we assume we were SSR'd and that
  590. // SSR did the initial scroll restoration.
  591. let initialScrollRestored = init.hydrationData != null;
  592. let initialMatches = matchRoutes(
  593. dataRoutes,
  594. init.history.location,
  595. init.basename
  596. );
  597. let initialErrors: RouteData | null = null;
  598. if (initialMatches == null) {
  599. // If we do not match a user-provided-route, fall back to the root
  600. // to allow the error boundary to take over
  601. let error = getInternalRouterError(404, {
  602. pathname: init.history.location.pathname,
  603. });
  604. let { matches, route } = getShortCircuitMatches(dataRoutes);
  605. initialMatches = matches;
  606. initialErrors = { [route.id]: error };
  607. }
  608. let initialized =
  609. !initialMatches.some((m) => m.route.loader) || init.hydrationData != null;
  610. let router: Router;
  611. let state: RouterState = {
  612. historyAction: init.history.action,
  613. location: init.history.location,
  614. matches: initialMatches,
  615. initialized,
  616. navigation: IDLE_NAVIGATION,
  617. // Don't restore on initial updateState() if we were SSR'd
  618. restoreScrollPosition: init.hydrationData != null ? false : null,
  619. preventScrollReset: false,
  620. revalidation: "idle",
  621. loaderData: (init.hydrationData && init.hydrationData.loaderData) || {},
  622. actionData: (init.hydrationData && init.hydrationData.actionData) || null,
  623. errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,
  624. fetchers: new Map(),
  625. blockers: new Map(),
  626. };
  627. // -- Stateful internal variables to manage navigations --
  628. // Current navigation in progress (to be committed in completeNavigation)
  629. let pendingAction: HistoryAction = HistoryAction.Pop;
  630. // Should the current navigation prevent the scroll reset if scroll cannot
  631. // be restored?
  632. let pendingPreventScrollReset = false;
  633. // AbortController for the active navigation
  634. let pendingNavigationController: AbortController | null;
  635. // We use this to avoid touching history in completeNavigation if a
  636. // revalidation is entirely uninterrupted
  637. let isUninterruptedRevalidation = false;
  638. // Use this internal flag to force revalidation of all loaders:
  639. // - submissions (completed or interrupted)
  640. // - useRevalidate()
  641. // - X-Remix-Revalidate (from redirect)
  642. let isRevalidationRequired = false;
  643. // Use this internal array to capture routes that require revalidation due
  644. // to a cancelled deferred on action submission
  645. let cancelledDeferredRoutes: string[] = [];
  646. // Use this internal array to capture fetcher loads that were cancelled by an
  647. // action navigation and require revalidation
  648. let cancelledFetcherLoads: string[] = [];
  649. // AbortControllers for any in-flight fetchers
  650. let fetchControllers = new Map<string, AbortController>();
  651. // Track loads based on the order in which they started
  652. let incrementingLoadId = 0;
  653. // Track the outstanding pending navigation data load to be compared against
  654. // the globally incrementing load when a fetcher load lands after a completed
  655. // navigation
  656. let pendingNavigationLoadId = -1;
  657. // Fetchers that triggered data reloads as a result of their actions
  658. let fetchReloadIds = new Map<string, number>();
  659. // Fetchers that triggered redirect navigations from their actions
  660. let fetchRedirectIds = new Set<string>();
  661. // Most recent href/match for fetcher.load calls for fetchers
  662. let fetchLoadMatches = new Map<string, FetchLoadMatch>();
  663. // Store DeferredData instances for active route matches. When a
  664. // route loader returns defer() we stick one in here. Then, when a nested
  665. // promise resolves we update loaderData. If a new navigation starts we
  666. // cancel active deferreds for eliminated routes.
  667. let activeDeferreds = new Map<string, DeferredData>();
  668. // Store blocker functions in a separate Map outside of router state since
  669. // we don't need to update UI state if they change
  670. let blockerFunctions = new Map<string, BlockerFunction>();
  671. // Flag to ignore the next history update, so we can revert the URL change on
  672. // a POP navigation that was blocked by the user without touching router state
  673. let ignoreNextHistoryUpdate = false;
  674. // Initialize the router, all side effects should be kicked off from here.
  675. // Implemented as a Fluent API for ease of:
  676. // let router = createRouter(init).initialize();
  677. function initialize() {
  678. // If history informs us of a POP navigation, start the navigation but do not update
  679. // state. We'll update our own state once the navigation completes
  680. unlistenHistory = init.history.listen(
  681. ({ action: historyAction, location, delta }) => {
  682. // Ignore this event if it was just us resetting the URL from a
  683. // blocked POP navigation
  684. if (ignoreNextHistoryUpdate) {
  685. ignoreNextHistoryUpdate = false;
  686. return;
  687. }
  688. warning(
  689. blockerFunctions.size === 0 || delta != null,
  690. "You are trying to use a blocker on a POP navigation to a location " +
  691. "that was not created by @remix-run/router. This will fail silently in " +
  692. "production. This can happen if you are navigating outside the router " +
  693. "via `window.history.pushState`/`window.location.hash` instead of using " +
  694. "router navigation APIs. This can also happen if you are using " +
  695. "createHashRouter and the user manually changes the URL."
  696. );
  697. let blockerKey = shouldBlockNavigation({
  698. currentLocation: state.location,
  699. nextLocation: location,
  700. historyAction,
  701. });
  702. if (blockerKey && delta != null) {
  703. // Restore the URL to match the current UI, but don't update router state
  704. ignoreNextHistoryUpdate = true;
  705. init.history.go(delta * -1);
  706. // Put the blocker into a blocked state
  707. updateBlocker(blockerKey, {
  708. state: "blocked",
  709. location,
  710. proceed() {
  711. updateBlocker(blockerKey!, {
  712. state: "proceeding",
  713. proceed: undefined,
  714. reset: undefined,
  715. location,
  716. });
  717. // Re-do the same POP navigation we just blocked
  718. init.history.go(delta);
  719. },
  720. reset() {
  721. deleteBlocker(blockerKey!);
  722. updateState({ blockers: new Map(router.state.blockers) });
  723. },
  724. });
  725. return;
  726. }
  727. return startNavigation(historyAction, location);
  728. }
  729. );
  730. // Kick off initial data load if needed. Use Pop to avoid modifying history
  731. if (!state.initialized) {
  732. startNavigation(HistoryAction.Pop, state.location);
  733. }
  734. return router;
  735. }
  736. // Clean up a router and it's side effects
  737. function dispose() {
  738. if (unlistenHistory) {
  739. unlistenHistory();
  740. }
  741. subscribers.clear();
  742. pendingNavigationController && pendingNavigationController.abort();
  743. state.fetchers.forEach((_, key) => deleteFetcher(key));
  744. state.blockers.forEach((_, key) => deleteBlocker(key));
  745. }
  746. // Subscribe to state updates for the router
  747. function subscribe(fn: RouterSubscriber) {
  748. subscribers.add(fn);
  749. return () => subscribers.delete(fn);
  750. }
  751. // Update our state and notify the calling context of the change
  752. function updateState(newState: Partial<RouterState>): void {
  753. state = {
  754. ...state,
  755. ...newState,
  756. };
  757. subscribers.forEach((subscriber) => subscriber(state));
  758. }
  759. // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
  760. // and setting state.[historyAction/location/matches] to the new route.
  761. // - Location is a required param
  762. // - Navigation will always be set to IDLE_NAVIGATION
  763. // - Can pass any other state in newState
  764. function completeNavigation(
  765. location: Location,
  766. newState: Partial<Omit<RouterState, "action" | "location" | "navigation">>
  767. ): void {
  768. // Deduce if we're in a loading/actionReload state:
  769. // - We have committed actionData in the store
  770. // - The current navigation was a mutation submission
  771. // - We're past the submitting state and into the loading state
  772. // - The location being loaded is not the result of a redirect
  773. let isActionReload =
  774. state.actionData != null &&
  775. state.navigation.formMethod != null &&
  776. isMutationMethod(state.navigation.formMethod) &&
  777. state.navigation.state === "loading" &&
  778. location.state?._isRedirect !== true;
  779. let actionData: RouteData | null;
  780. if (newState.actionData) {
  781. if (Object.keys(newState.actionData).length > 0) {
  782. actionData = newState.actionData;
  783. } else {
  784. // Empty actionData -> clear prior actionData due to an action error
  785. actionData = null;
  786. }
  787. } else if (isActionReload) {
  788. // Keep the current data if we're wrapping up the action reload
  789. actionData = state.actionData;
  790. } else {
  791. // Clear actionData on any other completed navigations
  792. actionData = null;
  793. }
  794. // Always preserve any existing loaderData from re-used routes
  795. let loaderData = newState.loaderData
  796. ? mergeLoaderData(
  797. state.loaderData,
  798. newState.loaderData,
  799. newState.matches || [],
  800. newState.errors
  801. )
  802. : state.loaderData;
  803. // On a successful navigation we can assume we got through all blockers
  804. // so we can start fresh
  805. for (let [key] of blockerFunctions) {
  806. deleteBlocker(key);
  807. }
  808. // Always respect the user flag. Otherwise don't reset on mutation
  809. // submission navigations unless they redirect
  810. let preventScrollReset =
  811. pendingPreventScrollReset === true ||
  812. (state.navigation.formMethod != null &&
  813. isMutationMethod(state.navigation.formMethod) &&
  814. location.state?._isRedirect !== true);
  815. updateState({
  816. ...newState, // matches, errors, fetchers go through as-is
  817. actionData,
  818. loaderData,
  819. historyAction: pendingAction,
  820. location,
  821. initialized: true,
  822. navigation: IDLE_NAVIGATION,
  823. revalidation: "idle",
  824. restoreScrollPosition: getSavedScrollPosition(
  825. location,
  826. newState.matches || state.matches
  827. ),
  828. preventScrollReset,
  829. blockers: new Map(state.blockers),
  830. });
  831. if (isUninterruptedRevalidation) {
  832. // If this was an uninterrupted revalidation then do not touch history
  833. } else if (pendingAction === HistoryAction.Pop) {
  834. // Do nothing for POP - URL has already been updated
  835. } else if (pendingAction === HistoryAction.Push) {
  836. init.history.push(location, location.state);
  837. } else if (pendingAction === HistoryAction.Replace) {
  838. init.history.replace(location, location.state);
  839. }
  840. // Reset stateful navigation vars
  841. pendingAction = HistoryAction.Pop;
  842. pendingPreventScrollReset = false;
  843. isUninterruptedRevalidation = false;
  844. isRevalidationRequired = false;
  845. cancelledDeferredRoutes = [];
  846. cancelledFetcherLoads = [];
  847. }
  848. // Trigger a navigation event, which can either be a numerical POP or a PUSH
  849. // replace with an optional submission
  850. async function navigate(
  851. to: number | To,
  852. opts?: RouterNavigateOptions
  853. ): Promise<void> {
  854. if (typeof to === "number") {
  855. init.history.go(to);
  856. return;
  857. }
  858. let { path, submission, error } = normalizeNavigateOptions(to, opts);
  859. let currentLocation = state.location;
  860. let nextLocation = createLocation(state.location, path, opts && opts.state);
  861. // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
  862. // URL from window.location, so we need to encode it here so the behavior
  863. // remains the same as POP and non-data-router usages. new URL() does all
  864. // the same encoding we'd get from a history.pushState/window.location read
  865. // without having to touch history
  866. nextLocation = {
  867. ...nextLocation,
  868. ...init.history.encodeLocation(nextLocation),
  869. };
  870. let userReplace = opts && opts.replace != null ? opts.replace : undefined;
  871. let historyAction = HistoryAction.Push;
  872. if (userReplace === true) {
  873. historyAction = HistoryAction.Replace;
  874. } else if (userReplace === false) {
  875. // no-op
  876. } else if (
  877. submission != null &&
  878. isMutationMethod(submission.formMethod) &&
  879. submission.formAction === state.location.pathname + state.location.search
  880. ) {
  881. // By default on submissions to the current location we REPLACE so that
  882. // users don't have to double-click the back button to get to the prior
  883. // location. If the user redirects to a different location from the
  884. // action/loader this will be ignored and the redirect will be a PUSH
  885. historyAction = HistoryAction.Replace;
  886. }
  887. let preventScrollReset =
  888. opts && "preventScrollReset" in opts
  889. ? opts.preventScrollReset === true
  890. : undefined;
  891. let blockerKey = shouldBlockNavigation({
  892. currentLocation,
  893. nextLocation,
  894. historyAction,
  895. });
  896. if (blockerKey) {
  897. // Put the blocker into a blocked state
  898. updateBlocker(blockerKey, {
  899. state: "blocked",
  900. location: nextLocation,
  901. proceed() {
  902. updateBlocker(blockerKey!, {
  903. state: "proceeding",
  904. proceed: undefined,
  905. reset: undefined,
  906. location: nextLocation,
  907. });
  908. // Send the same navigation through
  909. navigate(to, opts);
  910. },
  911. reset() {
  912. deleteBlocker(blockerKey!);
  913. updateState({ blockers: new Map(state.blockers) });
  914. },
  915. });
  916. return;
  917. }
  918. return await startNavigation(historyAction, nextLocation, {
  919. submission,
  920. // Send through the formData serialization error if we have one so we can
  921. // render at the right error boundary after we match routes
  922. pendingError: error,
  923. preventScrollReset,
  924. replace: opts && opts.replace,
  925. });
  926. }
  927. // Revalidate all current loaders. If a navigation is in progress or if this
  928. // is interrupted by a navigation, allow this to "succeed" by calling all
  929. // loaders during the next loader round
  930. function revalidate() {
  931. interruptActiveLoads();
  932. updateState({ revalidation: "loading" });
  933. // If we're currently submitting an action, we don't need to start a new
  934. // navigation, we'll just let the follow up loader execution call all loaders
  935. if (state.navigation.state === "submitting") {
  936. return;
  937. }
  938. // If we're currently in an idle state, start a new navigation for the current
  939. // action/location and mark it as uninterrupted, which will skip the history
  940. // update in completeNavigation
  941. if (state.navigation.state === "idle") {
  942. startNavigation(state.historyAction, state.location, {
  943. startUninterruptedRevalidation: true,
  944. });
  945. return;
  946. }
  947. // Otherwise, if we're currently in a loading state, just start a new
  948. // navigation to the navigation.location but do not trigger an uninterrupted
  949. // revalidation so that history correctly updates once the navigation completes
  950. startNavigation(
  951. pendingAction || state.historyAction,
  952. state.navigation.location,
  953. { overrideNavigation: state.navigation }
  954. );
  955. }
  956. // Start a navigation to the given action/location. Can optionally provide a
  957. // overrideNavigation which will override the normalLoad in the case of a redirect
  958. // navigation
  959. async function startNavigation(
  960. historyAction: HistoryAction,
  961. location: Location,
  962. opts?: {
  963. submission?: Submission;
  964. overrideNavigation?: Navigation;
  965. pendingError?: ErrorResponse;
  966. startUninterruptedRevalidation?: boolean;
  967. preventScrollReset?: boolean;
  968. replace?: boolean;
  969. }
  970. ): Promise<void> {
  971. // Abort any in-progress navigations and start a new one. Unset any ongoing
  972. // uninterrupted revalidations unless told otherwise, since we want this
  973. // new navigation to update history normally
  974. pendingNavigationController && pendingNavigationController.abort();
  975. pendingNavigationController = null;
  976. pendingAction = historyAction;
  977. isUninterruptedRevalidation =
  978. (opts && opts.startUninterruptedRevalidation) === true;
  979. // Save the current scroll position every time we start a new navigation,
  980. // and track whether we should reset scroll on completion
  981. saveScrollPosition(state.location, state.matches);
  982. pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
  983. let loadingNavigation = opts && opts.overrideNavigation;
  984. let matches = matchRoutes(dataRoutes, location, init.basename);
  985. // Short circuit with a 404 on the root error boundary if we match nothing
  986. if (!matches) {
  987. let error = getInternalRouterError(404, { pathname: location.pathname });
  988. let { matches: notFoundMatches, route } =
  989. getShortCircuitMatches(dataRoutes);
  990. // Cancel all pending deferred on 404s since we don't keep any routes
  991. cancelActiveDeferreds();
  992. completeNavigation(location, {
  993. matches: notFoundMatches,
  994. loaderData: {},
  995. errors: {
  996. [route.id]: error,
  997. },
  998. });
  999. return;
  1000. }
  1001. // Short circuit if it's only a hash change and not a mutation submission
  1002. // For example, on /page#hash and submit a <Form method="post"> which will
  1003. // default to a navigation to /page
  1004. if (
  1005. isHashChangeOnly(state.location, location) &&
  1006. !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))
  1007. ) {
  1008. completeNavigation(location, { matches });
  1009. return;
  1010. }
  1011. // Create a controller/Request for this navigation
  1012. pendingNavigationController = new AbortController();
  1013. let request = createClientSideRequest(
  1014. init.history,
  1015. location,
  1016. pendingNavigationController.signal,
  1017. opts && opts.submission
  1018. );
  1019. let pendingActionData: RouteData | undefined;
  1020. let pendingError: RouteData | undefined;
  1021. if (opts && opts.pendingError) {
  1022. // If we have a pendingError, it means the user attempted a GET submission
  1023. // with binary FormData so assign here and skip to handleLoaders. That
  1024. // way we handle calling loaders above the boundary etc. It's not really
  1025. // different from an actionError in that sense.
  1026. pendingError = {
  1027. [findNearestBoundary(matches).route.id]: opts.pendingError,
  1028. };
  1029. } else if (
  1030. opts &&
  1031. opts.submission &&
  1032. isMutationMethod(opts.submission.formMethod)
  1033. ) {
  1034. // Call action if we received an action submission
  1035. let actionOutput = await handleAction(
  1036. request,
  1037. location,
  1038. opts.submission,
  1039. matches,
  1040. { replace: opts.replace }
  1041. );
  1042. if (actionOutput.shortCircuited) {
  1043. return;
  1044. }
  1045. pendingActionData = actionOutput.pendingActionData;
  1046. pendingError = actionOutput.pendingActionError;
  1047. let navigation: NavigationStates["Loading"] = {
  1048. state: "loading",
  1049. location,
  1050. ...opts.submission,
  1051. };
  1052. loadingNavigation = navigation;
  1053. // Create a GET request for the loaders
  1054. request = new Request(request.url, { signal: request.signal });
  1055. }
  1056. // Call loaders
  1057. let { shortCircuited, loaderData, errors } = await handleLoaders(
  1058. request,
  1059. location,
  1060. matches,
  1061. loadingNavigation,
  1062. opts && opts.submission,
  1063. opts && opts.replace,
  1064. pendingActionData,
  1065. pendingError
  1066. );
  1067. if (shortCircuited) {
  1068. return;
  1069. }
  1070. // Clean up now that the action/loaders have completed. Don't clean up if
  1071. // we short circuited because pendingNavigationController will have already
  1072. // been assigned to a new controller for the next navigation
  1073. pendingNavigationController = null;
  1074. completeNavigation(location, {
  1075. matches,
  1076. ...(pendingActionData ? { actionData: pendingActionData } : {}),
  1077. loaderData,
  1078. errors,
  1079. });
  1080. }
  1081. // Call the action matched by the leaf route for this navigation and handle
  1082. // redirects/errors
  1083. async function handleAction(
  1084. request: Request,
  1085. location: Location,
  1086. submission: Submission,
  1087. matches: AgnosticDataRouteMatch[],
  1088. opts?: { replace?: boolean }
  1089. ): Promise<HandleActionResult> {
  1090. interruptActiveLoads();
  1091. // Put us in a submitting state
  1092. let navigation: NavigationStates["Submitting"] = {
  1093. state: "submitting",
  1094. location,
  1095. ...submission,
  1096. };
  1097. updateState({ navigation });
  1098. // Call our action and get the result
  1099. let result: DataResult;
  1100. let actionMatch = getTargetMatch(matches, location);
  1101. if (!actionMatch.route.action) {
  1102. result = {
  1103. type: ResultType.error,
  1104. error: getInternalRouterError(405, {
  1105. method: request.method,
  1106. pathname: location.pathname,
  1107. routeId: actionMatch.route.id,
  1108. }),
  1109. };
  1110. } else {
  1111. result = await callLoaderOrAction(
  1112. "action",
  1113. request,
  1114. actionMatch,
  1115. matches,
  1116. router.basename
  1117. );
  1118. if (request.signal.aborted) {
  1119. return { shortCircuited: true };
  1120. }
  1121. }
  1122. if (isRedirectResult(result)) {
  1123. let replace: boolean;
  1124. if (opts && opts.replace != null) {
  1125. replace = opts.replace;
  1126. } else {
  1127. // If the user didn't explicity indicate replace behavior, replace if
  1128. // we redirected to the exact same location we're currently at to avoid
  1129. // double back-buttons
  1130. replace =
  1131. result.location === state.location.pathname + state.location.search;
  1132. }
  1133. await startRedirectNavigation(state, result, { submission, replace });
  1134. return { shortCircuited: true };
  1135. }
  1136. if (isErrorResult(result)) {
  1137. // Store off the pending error - we use it to determine which loaders
  1138. // to call and will commit it when we complete the navigation
  1139. let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
  1140. // By default, all submissions are REPLACE navigations, but if the
  1141. // action threw an error that'll be rendered in an errorElement, we fall
  1142. // back to PUSH so that the user can use the back button to get back to
  1143. // the pre-submission form location to try again
  1144. if ((opts && opts.replace) !== true) {
  1145. pendingAction = HistoryAction.Push;
  1146. }
  1147. return {
  1148. // Send back an empty object we can use to clear out any prior actionData
  1149. pendingActionData: {},
  1150. pendingActionError: { [boundaryMatch.route.id]: result.error },
  1151. };
  1152. }
  1153. if (isDeferredResult(result)) {
  1154. throw getInternalRouterError(400, { type: "defer-action" });
  1155. }
  1156. return {
  1157. pendingActionData: { [actionMatch.route.id]: result.data },
  1158. };
  1159. }
  1160. // Call all applicable loaders for the given matches, handling redirects,
  1161. // errors, etc.
  1162. async function handleLoaders(
  1163. request: Request,
  1164. location: Location,
  1165. matches: AgnosticDataRouteMatch[],
  1166. overrideNavigation?: Navigation,
  1167. submission?: Submission,
  1168. replace?: boolean,
  1169. pendingActionData?: RouteData,
  1170. pendingError?: RouteData
  1171. ): Promise<HandleLoadersResult> {
  1172. // Figure out the right navigation we want to use for data loading
  1173. let loadingNavigation = overrideNavigation;
  1174. if (!loadingNavigation) {
  1175. let navigation: NavigationStates["Loading"] = {
  1176. state: "loading",
  1177. location,
  1178. formMethod: undefined,
  1179. formAction: undefined,
  1180. formEncType: undefined,
  1181. formData: undefined,
  1182. ...submission,
  1183. };
  1184. loadingNavigation = navigation;
  1185. }
  1186. // If this was a redirect from an action we don't have a "submission" but
  1187. // we have it on the loading navigation so use that if available
  1188. let activeSubmission = submission
  1189. ? submission
  1190. : loadingNavigation.formMethod &&
  1191. loadingNavigation.formAction &&
  1192. loadingNavigation.formData &&
  1193. loadingNavigation.formEncType
  1194. ? {
  1195. formMethod: loadingNavigation.formMethod,
  1196. formAction: loadingNavigation.formAction,
  1197. formData: loadingNavigation.formData,
  1198. formEncType: loadingNavigation.formEncType,
  1199. }
  1200. : undefined;
  1201. let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
  1202. init.history,
  1203. state,
  1204. matches,
  1205. activeSubmission,
  1206. location,
  1207. isRevalidationRequired,
  1208. cancelledDeferredRoutes,
  1209. cancelledFetcherLoads,
  1210. pendingActionData,
  1211. pendingError,
  1212. fetchLoadMatches
  1213. );
  1214. // Cancel pending deferreds for no-longer-matched routes or routes we're
  1215. // about to reload. Note that if this is an action reload we would have
  1216. // already cancelled all pending deferreds so this would be a no-op
  1217. cancelActiveDeferreds(
  1218. (routeId) =>
  1219. !(matches && matches.some((m) => m.route.id === routeId)) ||
  1220. (matchesToLoad && matchesToLoad.some((m) => m.route.id === routeId))
  1221. );
  1222. // Short circuit if we have no loaders to run
  1223. if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
  1224. completeNavigation(location, {
  1225. matches,
  1226. loaderData: {},
  1227. // Commit pending error if we're short circuiting
  1228. errors: pendingError || null,
  1229. ...(pendingActionData ? { actionData: pendingActionData } : {}),
  1230. });
  1231. return { shortCircuited: true };
  1232. }
  1233. // If this is an uninterrupted revalidation, we remain in our current idle
  1234. // state. If not, we need to switch to our loading state and load data,
  1235. // preserving any new action data or existing action data (in the case of
  1236. // a revalidation interrupting an actionReload)
  1237. if (!isUninterruptedRevalidation) {
  1238. revalidatingFetchers.forEach((rf) => {
  1239. let fetcher = state.fetchers.get(rf.key);
  1240. let revalidatingFetcher: FetcherStates["Loading"] = {
  1241. state: "loading",
  1242. data: fetcher && fetcher.data,
  1243. formMethod: undefined,
  1244. formAction: undefined,
  1245. formEncType: undefined,
  1246. formData: undefined,
  1247. " _hasFetcherDoneAnything ": true,
  1248. };
  1249. state.fetchers.set(rf.key, revalidatingFetcher);
  1250. });
  1251. let actionData = pendingActionData || state.actionData;
  1252. updateState({
  1253. navigation: loadingNavigation,
  1254. ...(actionData
  1255. ? Object.keys(actionData).length === 0
  1256. ? { actionData: null }
  1257. : { actionData }
  1258. : {}),
  1259. ...(revalidatingFetchers.length > 0
  1260. ? { fetchers: new Map(state.fetchers) }
  1261. : {}),
  1262. });
  1263. }
  1264. pendingNavigationLoadId = ++incrementingLoadId;
  1265. revalidatingFetchers.forEach((rf) =>
  1266. fetchControllers.set(rf.key, pendingNavigationController!)
  1267. );
  1268. let { results, loaderResults, fetcherResults } =
  1269. await callLoadersAndMaybeResolveData(
  1270. state.matches,
  1271. matches,
  1272. matchesToLoad,
  1273. revalidatingFetchers,
  1274. request
  1275. );
  1276. if (request.signal.aborted) {
  1277. return { shortCircuited: true };
  1278. }
  1279. // Clean up _after_ loaders have completed. Don't clean up if we short
  1280. // circuited because fetchControllers would have been aborted and
  1281. // reassigned to new controllers for the next navigation
  1282. revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
  1283. // If any loaders returned a redirect Response, start a new REPLACE navigation
  1284. let redirect = findRedirect(results);
  1285. if (redirect) {
  1286. await startRedirectNavigation(state, redirect, { replace });
  1287. return { shortCircuited: true };
  1288. }
  1289. // Process and commit output from loaders
  1290. let { loaderData, errors } = processLoaderData(
  1291. state,
  1292. matches,
  1293. matchesToLoad,
  1294. loaderResults,
  1295. pendingError,
  1296. revalidatingFetchers,
  1297. fetcherResults,
  1298. activeDeferreds
  1299. );
  1300. // Wire up subscribers to update loaderData as promises settle
  1301. activeDeferreds.forEach((deferredData, routeId) => {
  1302. deferredData.subscribe((aborted) => {
  1303. // Note: No need to updateState here since the TrackedPromise on
  1304. // loaderData is stable across resolve/reject
  1305. // Remove this instance if we were aborted or if promises have settled
  1306. if (aborted || deferredData.done) {
  1307. activeDeferreds.delete(routeId);
  1308. }
  1309. });
  1310. });
  1311. markFetchRedirectsDone();
  1312. let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
  1313. return {
  1314. loaderData,
  1315. errors,
  1316. ...(didAbortFetchLoads || revalidatingFetchers.length > 0
  1317. ? { fetchers: new Map(state.fetchers) }
  1318. : {}),
  1319. };
  1320. }
  1321. function getFetcher<TData = any>(key: string): Fetcher<TData> {
  1322. return state.fetchers.get(key) || IDLE_FETCHER;
  1323. }
  1324. // Trigger a fetcher load/submit for the given fetcher key
  1325. function fetch(
  1326. key: string,
  1327. routeId: string,
  1328. href: string,
  1329. opts?: RouterFetchOptions
  1330. ) {
  1331. if (isServer) {
  1332. throw new Error(
  1333. "router.fetch() was called during the server render, but it shouldn't be. " +
  1334. "You are likely calling a useFetcher() method in the body of your component. " +
  1335. "Try moving it to a useEffect or a callback."
  1336. );
  1337. }
  1338. if (fetchControllers.has(key)) abortFetcher(key);
  1339. let matches = matchRoutes(dataRoutes, href, init.basename);
  1340. if (!matches) {
  1341. setFetcherError(
  1342. key,
  1343. routeId,
  1344. getInternalRouterError(404, { pathname: href })
  1345. );
  1346. return;
  1347. }
  1348. let { path, submission } = normalizeNavigateOptions(href, opts, true);
  1349. let match = getTargetMatch(matches, path);
  1350. pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
  1351. if (submission && isMutationMethod(submission.formMethod)) {
  1352. handleFetcherAction(key, routeId, path, match, matches, submission);
  1353. return;
  1354. }
  1355. // Store off the match so we can call it's shouldRevalidate on subsequent
  1356. // revalidations
  1357. fetchLoadMatches.set(key, { routeId, path, match, matches });
  1358. handleFetcherLoader(key, routeId, path, match, matches, submission);
  1359. }
  1360. // Call the action for the matched fetcher.submit(), and then handle redirects,
  1361. // errors, and revalidation
  1362. async function handleFetcherAction(
  1363. key: string,
  1364. routeId: string,
  1365. path: string,
  1366. match: AgnosticDataRouteMatch,
  1367. requestMatches: AgnosticDataRouteMatch[],
  1368. submission: Submission
  1369. ) {
  1370. interruptActiveLoads();
  1371. fetchLoadMatches.delete(key);
  1372. if (!match.route.action) {
  1373. let error = getInternalRouterError(405, {
  1374. method: submission.formMethod,
  1375. pathname: path,
  1376. routeId: routeId,
  1377. });
  1378. setFetcherError(key, routeId, error);
  1379. return;
  1380. }
  1381. // Put this fetcher into it's submitting state
  1382. let existingFetcher = state.fetchers.get(key);
  1383. let fetcher: FetcherStates["Submitting"] = {
  1384. state: "submitting",
  1385. ...submission,
  1386. data: existingFetcher && existingFetcher.data,
  1387. " _hasFetcherDoneAnything ": true,
  1388. };
  1389. state.fetchers.set(key, fetcher);
  1390. updateState({ fetchers: new Map(state.fetchers) });
  1391. // Call the action for the fetcher
  1392. let abortController = new AbortController();
  1393. let fetchRequest = createClientSideRequest(
  1394. init.history,
  1395. path,
  1396. abortController.signal,
  1397. submission
  1398. );
  1399. fetchControllers.set(key, abortController);
  1400. let actionResult = await callLoaderOrAction(
  1401. "action",
  1402. fetchRequest,
  1403. match,
  1404. requestMatches,
  1405. router.basename
  1406. );
  1407. if (fetchRequest.signal.aborted) {
  1408. // We can delete this so long as we weren't aborted by ou our own fetcher
  1409. // re-submit which would have put _new_ controller is in fetchControllers
  1410. if (fetchControllers.get(key) === abortController) {
  1411. fetchControllers.delete(key);
  1412. }
  1413. return;
  1414. }
  1415. if (isRedirectResult(actionResult)) {
  1416. fetchControllers.delete(key);
  1417. fetchRedirectIds.add(key);
  1418. let loadingFetcher: FetcherStates["Loading"] = {
  1419. state: "loading",
  1420. ...submission,
  1421. data: undefined,
  1422. " _hasFetcherDoneAnything ": true,
  1423. };
  1424. state.fetchers.set(key, loadingFetcher);
  1425. updateState({ fetchers: new Map(state.fetchers) });
  1426. return startRedirectNavigation(state, actionResult, {
  1427. isFetchActionRedirect: true,
  1428. });
  1429. }
  1430. // Process any non-redirect errors thrown
  1431. if (isErrorResult(actionResult)) {
  1432. setFetcherError(key, routeId, actionResult.error);
  1433. return;
  1434. }
  1435. if (isDeferredResult(actionResult)) {
  1436. throw getInternalRouterError(400, { type: "defer-action" });
  1437. }
  1438. // Start the data load for current matches, or the next location if we're
  1439. // in the middle of a navigation
  1440. let nextLocation = state.navigation.location || state.location;
  1441. let revalidationRequest = createClientSideRequest(
  1442. init.history,
  1443. nextLocation,
  1444. abortController.signal
  1445. );
  1446. let matches =
  1447. state.navigation.state !== "idle"
  1448. ? matchRoutes(dataRoutes, state.navigation.location, init.basename)
  1449. : state.matches;
  1450. invariant(matches, "Didn't find any matches after fetcher action");
  1451. let loadId = ++incrementingLoadId;
  1452. fetchReloadIds.set(key, loadId);
  1453. let loadFetcher: FetcherStates["Loading"] = {
  1454. state: "loading",
  1455. data: actionResult.data,
  1456. ...submission,
  1457. " _hasFetcherDoneAnything ": true,
  1458. };
  1459. state.fetchers.set(key, loadFetcher);
  1460. let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
  1461. init.history,
  1462. state,
  1463. matches,
  1464. submission,
  1465. nextLocation,
  1466. isRevalidationRequired,
  1467. cancelledDeferredRoutes,
  1468. cancelledFetcherLoads,
  1469. { [match.route.id]: actionResult.data },
  1470. undefined, // No need to send through errors since we short circuit above
  1471. fetchLoadMatches
  1472. );
  1473. // Put all revalidating fetchers into the loading state, except for the
  1474. // current fetcher which we want to keep in it's current loading state which
  1475. // contains it's action submission info + action data
  1476. revalidatingFetchers
  1477. .filter((rf) => rf.key !== key)
  1478. .forEach((rf) => {
  1479. let staleKey = rf.key;
  1480. let existingFetcher = state.fetchers.get(staleKey);
  1481. let revalidatingFetcher: FetcherStates["Loading"] = {
  1482. state: "loading",
  1483. data: existingFetcher && existingFetcher.data,
  1484. formMethod: undefined,
  1485. formAction: undefined,
  1486. formEncType: undefined,
  1487. formData: undefined,
  1488. " _hasFetcherDoneAnything ": true,
  1489. };
  1490. state.fetchers.set(staleKey, revalidatingFetcher);
  1491. fetchControllers.set(staleKey, abortController);
  1492. });
  1493. updateState({ fetchers: new Map(state.fetchers) });
  1494. let { results, loaderResults, fetcherResults } =
  1495. await callLoadersAndMaybeResolveData(
  1496. state.matches,
  1497. matches,
  1498. matchesToLoad,
  1499. revalidatingFetchers,
  1500. revalidationRequest
  1501. );
  1502. if (abortController.signal.aborted) {
  1503. return;
  1504. }
  1505. fetchReloadIds.delete(key);
  1506. fetchControllers.delete(key);
  1507. revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
  1508. let redirect = findRedirect(results);
  1509. if (redirect) {
  1510. return startRedirectNavigation(state, redirect);
  1511. }
  1512. // Process and commit output from loaders
  1513. let { loaderData, errors } = processLoaderData(
  1514. state,
  1515. state.matches,
  1516. matchesToLoad,
  1517. loaderResults,
  1518. undefined,
  1519. revalidatingFetchers,
  1520. fetcherResults,
  1521. activeDeferreds
  1522. );
  1523. let doneFetcher: FetcherStates["Idle"] = {
  1524. state: "idle",
  1525. data: actionResult.data,
  1526. formMethod: undefined,
  1527. formAction: undefined,
  1528. formEncType: undefined,
  1529. formData: undefined,
  1530. " _hasFetcherDoneAnything ": true,
  1531. };
  1532. state.fetchers.set(key, doneFetcher);
  1533. let didAbortFetchLoads = abortStaleFetchLoads(loadId);
  1534. // If we are currently in a navigation loading state and this fetcher is
  1535. // more recent than the navigation, we want the newer data so abort the
  1536. // navigation and complete it with the fetcher data
  1537. if (
  1538. state.navigation.state === "loading" &&
  1539. loadId > pendingNavigationLoadId
  1540. ) {
  1541. invariant(pendingAction, "Expected pending action");
  1542. pendingNavigationController && pendingNavigationController.abort();
  1543. completeNavigation(state.navigation.location, {
  1544. matches,
  1545. loaderData,
  1546. errors,
  1547. fetchers: new Map(state.fetchers),
  1548. });
  1549. } else {
  1550. // otherwise just update with the fetcher data, preserving any existing
  1551. // loaderData for loaders that did not need to reload. We have to
  1552. // manually merge here since we aren't going through completeNavigation
  1553. updateState({
  1554. errors,
  1555. loaderData: mergeLoaderData(
  1556. state.loaderData,
  1557. loaderData,
  1558. matches,
  1559. errors
  1560. ),
  1561. ...(didAbortFetchLoads ? { fetchers: new Map(state.fetchers) } : {}),
  1562. });
  1563. isRevalidationRequired = false;
  1564. }
  1565. }
  1566. // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
  1567. async function handleFetcherLoader(
  1568. key: string,
  1569. routeId: string,
  1570. path: string,
  1571. match: AgnosticDataRouteMatch,
  1572. matches: AgnosticDataRouteMatch[],
  1573. submission?: Submission
  1574. ) {
  1575. let existingFetcher = state.fetchers.get(key);
  1576. // Put this fetcher into it's loading state
  1577. let loadingFetcher: FetcherStates["Loading"] = {
  1578. state: "loading",
  1579. formMethod: undefined,
  1580. formAction: undefined,
  1581. formEncType: undefined,
  1582. formData: undefined,
  1583. ...submission,
  1584. data: existingFetcher && existingFetcher.data,
  1585. " _hasFetcherDoneAnything ": true,
  1586. };
  1587. state.fetchers.set(key, loadingFetcher);
  1588. updateState({ fetchers: new Map(state.fetchers) });
  1589. // Call the loader for this fetcher route match
  1590. let abortController = new AbortController();
  1591. let fetchRequest = createClientSideRequest(
  1592. init.history,
  1593. path,
  1594. abortController.signal
  1595. );
  1596. fetchControllers.set(key, abortController);
  1597. let result: DataResult = await callLoaderOrAction(
  1598. "loader",
  1599. fetchRequest,
  1600. match,
  1601. matches,
  1602. router.basename
  1603. );
  1604. // Deferred isn't supported for fetcher loads, await everything and treat it
  1605. // as a normal load. resolveDeferredData will return undefined if this
  1606. // fetcher gets aborted, so we just leave result untouched and short circuit
  1607. // below if that happens
  1608. if (isDeferredResult(result)) {
  1609. result =
  1610. (await resolveDeferredData(result, fetchRequest.signal, true)) ||
  1611. result;
  1612. }
  1613. // We can delete this so long as we weren't aborted by ou our own fetcher
  1614. // re-load which would have put _new_ controller is in fetchControllers
  1615. if (fetchControllers.get(key) === abortController) {
  1616. fetchControllers.delete(key);
  1617. }
  1618. if (fetchRequest.signal.aborted) {
  1619. return;
  1620. }
  1621. // If the loader threw a redirect Response, start a new REPLACE navigation
  1622. if (isRedirectResult(result)) {
  1623. await startRedirectNavigation(state, result);
  1624. return;
  1625. }
  1626. // Process any non-redirect errors thrown
  1627. if (isErrorResult(result)) {
  1628. let boundaryMatch = findNearestBoundary(state.matches, routeId);
  1629. state.fetchers.delete(key);
  1630. // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
  1631. // do we need to behave any differently with our non-redirect errors?
  1632. // What if it was a non-redirect Response?
  1633. updateState({
  1634. fetchers: new Map(state.fetchers),
  1635. errors: {
  1636. [boundaryMatch.route.id]: result.error,
  1637. },
  1638. });
  1639. return;
  1640. }
  1641. invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
  1642. // Put the fetcher back into an idle state
  1643. let doneFetcher: FetcherStates["Idle"] = {
  1644. state: "idle",
  1645. data: result.data,
  1646. formMethod: undefined,
  1647. formAction: undefined,
  1648. formEncType: undefined,
  1649. formData: undefined,
  1650. " _hasFetcherDoneAnything ": true,
  1651. };
  1652. state.fetchers.set(key, doneFetcher);
  1653. updateState({ fetchers: new Map(state.fetchers) });
  1654. }
  1655. /**
  1656. * Utility function to handle redirects returned from an action or loader.
  1657. * Normally, a redirect "replaces" the navigation that triggered it. So, for
  1658. * example:
  1659. *
  1660. * - user is on /a
  1661. * - user clicks a link to /b
  1662. * - loader for /b redirects to /c
  1663. *
  1664. * In a non-JS app the browser would track the in-flight navigation to /b and
  1665. * then replace it with /c when it encountered the redirect response. In
  1666. * the end it would only ever update the URL bar with /c.
  1667. *
  1668. * In client-side routing using pushState/replaceState, we aim to emulate
  1669. * this behavior and we also do not update history until the end of the
  1670. * navigation (including processed redirects). This means that we never
  1671. * actually touch history until we've processed redirects, so we just use
  1672. * the history action from the original navigation (PUSH or REPLACE).
  1673. */
  1674. async function startRedirectNavigation(
  1675. state: RouterState,
  1676. redirect: RedirectResult,
  1677. {
  1678. submission,
  1679. replace,
  1680. isFetchActionRedirect,
  1681. }: {
  1682. submission?: Submission;
  1683. replace?: boolean;
  1684. isFetchActionRedirect?: boolean;
  1685. } = {}
  1686. ) {
  1687. if (redirect.revalidate) {
  1688. isRevalidationRequired = true;
  1689. }
  1690. let redirectLocation = createLocation(
  1691. state.location,
  1692. redirect.location,
  1693. // TODO: This can be removed once we get rid of useTransition in Remix v2
  1694. {
  1695. _isRedirect: true,
  1696. ...(isFetchActionRedirect ? { _isFetchActionRedirect: true } : {}),
  1697. }
  1698. );
  1699. invariant(
  1700. redirectLocation,
  1701. "Expected a location on the redirect navigation"
  1702. );
  1703. // Check if this an absolute external redirect that goes to a new origin
  1704. if (
  1705. ABSOLUTE_URL_REGEX.test(redirect.location) &&
  1706. isBrowser &&
  1707. typeof window?.location !== "undefined"
  1708. ) {
  1709. let newOrigin = init.history.createURL(redirect.location).origin;
  1710. if (window.location.origin !== newOrigin) {
  1711. if (replace) {
  1712. window.location.replace(redirect.location);
  1713. } else {
  1714. window.location.assign(redirect.location);
  1715. }
  1716. return;
  1717. }
  1718. }
  1719. // There's no need to abort on redirects, since we don't detect the
  1720. // redirect until the action/loaders have settled
  1721. pendingNavigationController = null;
  1722. let redirectHistoryAction =
  1723. replace === true ? HistoryAction.Replace : HistoryAction.Push;
  1724. // Use the incoming submission if provided, fallback on the active one in
  1725. // state.navigation
  1726. let { formMethod, formAction, formEncType, formData } = state.navigation;
  1727. if (!submission && formMethod && formAction && formData && formEncType) {
  1728. submission = {
  1729. formMethod,
  1730. formAction,
  1731. formEncType,
  1732. formData,
  1733. };
  1734. }
  1735. // If this was a 307/308 submission we want to preserve the HTTP method and
  1736. // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
  1737. // redirected location
  1738. if (
  1739. redirectPreserveMethodStatusCodes.has(redirect.status) &&
  1740. submission &&
  1741. isMutationMethod(submission.formMethod)
  1742. ) {
  1743. await startNavigation(redirectHistoryAction, redirectLocation, {
  1744. submission: {
  1745. ...submission,
  1746. formAction: redirect.location,
  1747. },
  1748. // Preserve this flag across redirects
  1749. preventScrollReset: pendingPreventScrollReset,
  1750. });
  1751. } else {
  1752. // Otherwise, we kick off a new loading navigation, preserving the
  1753. // submission info for the duration of this navigation
  1754. await startNavigation(redirectHistoryAction, redirectLocation, {
  1755. overrideNavigation: {
  1756. state: "loading",
  1757. location: redirectLocation,
  1758. formMethod: submission ? submission.formMethod : undefined,
  1759. formAction: submission ? submission.formAction : undefined,
  1760. formEncType: submission ? submission.formEncType : undefined,
  1761. formData: submission ? submission.formData : undefined,
  1762. },
  1763. // Preserve this flag across redirects
  1764. preventScrollReset: pendingPreventScrollReset,
  1765. });
  1766. }
  1767. }
  1768. async function callLoadersAndMaybeResolveData(
  1769. currentMatches: AgnosticDataRouteMatch[],
  1770. matches: AgnosticDataRouteMatch[],
  1771. matchesToLoad: AgnosticDataRouteMatch[],
  1772. fetchersToLoad: RevalidatingFetcher[],
  1773. request: Request
  1774. ) {
  1775. // Call all navigation loaders and revalidating fetcher loaders in parallel,
  1776. // then slice off the results into separate arrays so we can handle them
  1777. // accordingly
  1778. let results = await Promise.all([
  1779. ...matchesToLoad.map((match) =>
  1780. callLoaderOrAction("loader", request, match, matches, router.basename)
  1781. ),
  1782. ...fetchersToLoad.map((f) =>
  1783. callLoaderOrAction(
  1784. "loader",
  1785. createClientSideRequest(init.history, f.path, request.signal),
  1786. f.match,
  1787. f.matches,
  1788. router.basename
  1789. )
  1790. ),
  1791. ]);
  1792. let loaderResults = results.slice(0, matchesToLoad.length);
  1793. let fetcherResults = results.slice(matchesToLoad.length);
  1794. await Promise.all([
  1795. resolveDeferredResults(
  1796. currentMatches,
  1797. matchesToLoad,
  1798. loaderResults,
  1799. request.signal,
  1800. false,
  1801. state.loaderData
  1802. ),
  1803. resolveDeferredResults(
  1804. currentMatches,
  1805. fetchersToLoad.map((f) => f.match),
  1806. fetcherResults,
  1807. request.signal,
  1808. true
  1809. ),
  1810. ]);
  1811. return { results, loaderResults, fetcherResults };
  1812. }
  1813. function interruptActiveLoads() {
  1814. // Every interruption triggers a revalidation
  1815. isRevalidationRequired = true;
  1816. // Cancel pending route-level deferreds and mark cancelled routes for
  1817. // revalidation
  1818. cancelledDeferredRoutes.push(...cancelActiveDeferreds());
  1819. // Abort in-flight fetcher loads
  1820. fetchLoadMatches.forEach((_, key) => {
  1821. if (fetchControllers.has(key)) {
  1822. cancelledFetcherLoads.push(key);
  1823. abortFetcher(key);
  1824. }
  1825. });
  1826. }
  1827. function setFetcherError(key: string, routeId: string, error: any) {
  1828. let boundaryMatch = findNearestBoundary(state.matches, routeId);
  1829. deleteFetcher(key);
  1830. updateState({
  1831. errors: {
  1832. [boundaryMatch.route.id]: error,
  1833. },
  1834. fetchers: new Map(state.fetchers),
  1835. });
  1836. }
  1837. function deleteFetcher(key: string): void {
  1838. if (fetchControllers.has(key)) abortFetcher(key);
  1839. fetchLoadMatches.delete(key);
  1840. fetchReloadIds.delete(key);
  1841. fetchRedirectIds.delete(key);
  1842. state.fetchers.delete(key);
  1843. }
  1844. function abortFetcher(key: string) {
  1845. let controller = fetchControllers.get(key);
  1846. invariant(controller, `Expected fetch controller: ${key}`);
  1847. controller.abort();
  1848. fetchControllers.delete(key);
  1849. }
  1850. function markFetchersDone(keys: string[]) {
  1851. for (let key of keys) {
  1852. let fetcher = getFetcher(key);
  1853. let doneFetcher: FetcherStates["Idle"] = {
  1854. state: "idle",
  1855. data: fetcher.data,
  1856. formMethod: undefined,
  1857. formAction: undefined,
  1858. formEncType: undefined,
  1859. formData: undefined,
  1860. " _hasFetcherDoneAnything ": true,
  1861. };
  1862. state.fetchers.set(key, doneFetcher);
  1863. }
  1864. }
  1865. function markFetchRedirectsDone(): void {
  1866. let doneKeys = [];
  1867. for (let key of fetchRedirectIds) {
  1868. let fetcher = state.fetchers.get(key);
  1869. invariant(fetcher, `Expected fetcher: ${key}`);
  1870. if (fetcher.state === "loading") {
  1871. fetchRedirectIds.delete(key);
  1872. doneKeys.push(key);
  1873. }
  1874. }
  1875. markFetchersDone(doneKeys);
  1876. }
  1877. function abortStaleFetchLoads(landedId: number): boolean {
  1878. let yeetedKeys = [];
  1879. for (let [key, id] of fetchReloadIds) {
  1880. if (id < landedId) {
  1881. let fetcher = state.fetchers.get(key);
  1882. invariant(fetcher, `Expected fetcher: ${key}`);
  1883. if (fetcher.state === "loading") {
  1884. abortFetcher(key);
  1885. fetchReloadIds.delete(key);
  1886. yeetedKeys.push(key);
  1887. }
  1888. }
  1889. }
  1890. markFetchersDone(yeetedKeys);
  1891. return yeetedKeys.length > 0;
  1892. }
  1893. function getBlocker(key: string, fn: BlockerFunction) {
  1894. let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;
  1895. if (blockerFunctions.get(key) !== fn) {
  1896. blockerFunctions.set(key, fn);
  1897. }
  1898. return blocker;
  1899. }
  1900. function deleteBlocker(key: string) {
  1901. state.blockers.delete(key);
  1902. blockerFunctions.delete(key);
  1903. }
  1904. // Utility function to update blockers, ensuring valid state transitions
  1905. function updateBlocker(key: string, newBlocker: Blocker) {
  1906. let blocker = state.blockers.get(key) || IDLE_BLOCKER;
  1907. // Poor mans state machine :)
  1908. // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
  1909. invariant(
  1910. (blocker.state === "unblocked" && newBlocker.state === "blocked") ||
  1911. (blocker.state === "blocked" && newBlocker.state === "blocked") ||
  1912. (blocker.state === "blocked" && newBlocker.state === "proceeding") ||
  1913. (blocker.state === "blocked" && newBlocker.state === "unblocked") ||
  1914. (blocker.state === "proceeding" && newBlocker.state === "unblocked"),
  1915. `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`
  1916. );
  1917. state.blockers.set(key, newBlocker);
  1918. updateState({ blockers: new Map(state.blockers) });
  1919. }
  1920. function shouldBlockNavigation({
  1921. currentLocation,
  1922. nextLocation,
  1923. historyAction,
  1924. }: {
  1925. currentLocation: Location;
  1926. nextLocation: Location;
  1927. historyAction: HistoryAction;
  1928. }): string | undefined {
  1929. if (blockerFunctions.size === 0) {
  1930. return;
  1931. }
  1932. // We ony support a single active blocker at the moment since we don't have
  1933. // any compelling use cases for multi-blocker yet
  1934. if (blockerFunctions.size > 1) {
  1935. warning(false, "A router only supports one blocker at a time");
  1936. }
  1937. let entries = Array.from(blockerFunctions.entries());
  1938. let [blockerKey, blockerFunction] = entries[entries.length - 1];
  1939. let blocker = state.blockers.get(blockerKey);
  1940. if (blocker && blocker.state === "proceeding") {
  1941. // If the blocker is currently proceeding, we don't need to re-check
  1942. // it and can let this navigation continue
  1943. return;
  1944. }
  1945. // At this point, we know we're unblocked/blocked so we need to check the
  1946. // user-provided blocker function
  1947. if (blockerFunction({ currentLocation, nextLocation, historyAction })) {
  1948. return blockerKey;
  1949. }
  1950. }
  1951. function cancelActiveDeferreds(
  1952. predicate?: (routeId: string) => boolean
  1953. ): string[] {
  1954. let cancelledRouteIds: string[] = [];
  1955. activeDeferreds.forEach((dfd, routeId) => {
  1956. if (!predicate || predicate(routeId)) {
  1957. // Cancel the deferred - but do not remove from activeDeferreds here -
  1958. // we rely on the subscribers to do that so our tests can assert proper
  1959. // cleanup via _internalActiveDeferreds
  1960. dfd.cancel();
  1961. cancelledRouteIds.push(routeId);
  1962. activeDeferreds.delete(routeId);
  1963. }
  1964. });
  1965. return cancelledRouteIds;
  1966. }
  1967. // Opt in to capturing and reporting scroll positions during navigations,
  1968. // used by the <ScrollRestoration> component
  1969. function enableScrollRestoration(
  1970. positions: Record<string, number>,
  1971. getPosition: GetScrollPositionFunction,
  1972. getKey?: GetScrollRestorationKeyFunction
  1973. ) {
  1974. savedScrollPositions = positions;
  1975. getScrollPosition = getPosition;
  1976. getScrollRestorationKey = getKey || ((location) => location.key);
  1977. // Perform initial hydration scroll restoration, since we miss the boat on
  1978. // the initial updateState() because we've not yet rendered <ScrollRestoration/>
  1979. // and therefore have no savedScrollPositions available
  1980. if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
  1981. initialScrollRestored = true;
  1982. let y = getSavedScrollPosition(state.location, state.matches);
  1983. if (y != null) {
  1984. updateState({ restoreScrollPosition: y });
  1985. }
  1986. }
  1987. return () => {
  1988. savedScrollPositions = null;
  1989. getScrollPosition = null;
  1990. getScrollRestorationKey = null;
  1991. };
  1992. }
  1993. function saveScrollPosition(
  1994. location: Location,
  1995. matches: AgnosticDataRouteMatch[]
  1996. ): void {
  1997. if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
  1998. let userMatches = matches.map((m) =>
  1999. createUseMatchesMatch(m, state.loaderData)
  2000. );
  2001. let key = getScrollRestorationKey(location, userMatches) || location.key;
  2002. savedScrollPositions[key] = getScrollPosition();
  2003. }
  2004. }
  2005. function getSavedScrollPosition(
  2006. location: Location,
  2007. matches: AgnosticDataRouteMatch[]
  2008. ): number | null {
  2009. if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
  2010. let userMatches = matches.map((m) =>
  2011. createUseMatchesMatch(m, state.loaderData)
  2012. );
  2013. let key = getScrollRestorationKey(location, userMatches) || location.key;
  2014. let y = savedScrollPositions[key];
  2015. if (typeof y === "number") {
  2016. return y;
  2017. }
  2018. }
  2019. return null;
  2020. }
  2021. router = {
  2022. get basename() {
  2023. return init.basename;
  2024. },
  2025. get state() {
  2026. return state;
  2027. },
  2028. get routes() {
  2029. return dataRoutes;
  2030. },
  2031. initialize,
  2032. subscribe,
  2033. enableScrollRestoration,
  2034. navigate,
  2035. fetch,
  2036. revalidate,
  2037. // Passthrough to history-aware createHref used by useHref so we get proper
  2038. // hash-aware URLs in DOM paths
  2039. createHref: (to: To) => init.history.createHref(to),
  2040. encodeLocation: (to: To) => init.history.encodeLocation(to),
  2041. getFetcher,
  2042. deleteFetcher,
  2043. dispose,
  2044. getBlocker,
  2045. deleteBlocker,
  2046. _internalFetchControllers: fetchControllers,
  2047. _internalActiveDeferreds: activeDeferreds,
  2048. };
  2049. return router;
  2050. }
  2051. //#endregion
  2052. ////////////////////////////////////////////////////////////////////////////////
  2053. //#region createStaticHandler
  2054. ////////////////////////////////////////////////////////////////////////////////
  2055. export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
  2056. export function createStaticHandler(
  2057. routes: AgnosticRouteObject[],
  2058. opts?: {
  2059. basename?: string;
  2060. }
  2061. ): StaticHandler {
  2062. invariant(
  2063. routes.length > 0,
  2064. "You must provide a non-empty routes array to createStaticHandler"
  2065. );
  2066. let dataRoutes = convertRoutesToDataRoutes(routes);
  2067. let basename = (opts ? opts.basename : null) || "/";
  2068. /**
  2069. * The query() method is intended for document requests, in which we want to
  2070. * call an optional action and potentially multiple loaders for all nested
  2071. * routes. It returns a StaticHandlerContext object, which is very similar
  2072. * to the router state (location, loaderData, actionData, errors, etc.) and
  2073. * also adds SSR-specific information such as the statusCode and headers
  2074. * from action/loaders Responses.
  2075. *
  2076. * It _should_ never throw and should report all errors through the
  2077. * returned context.errors object, properly associating errors to their error
  2078. * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
  2079. * used to emulate React error boundaries during SSr by performing a second
  2080. * pass only down to the boundaryId.
  2081. *
  2082. * The one exception where we do not return a StaticHandlerContext is when a
  2083. * redirect response is returned or thrown from any action/loader. We
  2084. * propagate that out and return the raw Response so the HTTP server can
  2085. * return it directly.
  2086. */
  2087. async function query(
  2088. request: Request,
  2089. { requestContext }: { requestContext?: unknown } = {}
  2090. ): Promise<StaticHandlerContext | Response> {
  2091. let url = new URL(request.url);
  2092. let method = request.method.toLowerCase();
  2093. let location = createLocation("", createPath(url), null, "default");
  2094. let matches = matchRoutes(dataRoutes, location, basename);
  2095. // SSR supports HEAD requests while SPA doesn't
  2096. if (!isValidMethod(method) && method !== "head") {
  2097. let error = getInternalRouterError(405, { method });
  2098. let { matches: methodNotAllowedMatches, route } =
  2099. getShortCircuitMatches(dataRoutes);
  2100. return {
  2101. basename,
  2102. location,
  2103. matches: methodNotAllowedMatches,
  2104. loaderData: {},
  2105. actionData: null,
  2106. errors: {
  2107. [route.id]: error,
  2108. },
  2109. statusCode: error.status,
  2110. loaderHeaders: {},
  2111. actionHeaders: {},
  2112. activeDeferreds: null,
  2113. };
  2114. } else if (!matches) {
  2115. let error = getInternalRouterError(404, { pathname: location.pathname });
  2116. let { matches: notFoundMatches, route } =
  2117. getShortCircuitMatches(dataRoutes);
  2118. return {
  2119. basename,
  2120. location,
  2121. matches: notFoundMatches,
  2122. loaderData: {},
  2123. actionData: null,
  2124. errors: {
  2125. [route.id]: error,
  2126. },
  2127. statusCode: error.status,
  2128. loaderHeaders: {},
  2129. actionHeaders: {},
  2130. activeDeferreds: null,
  2131. };
  2132. }
  2133. let result = await queryImpl(request, location, matches, requestContext);
  2134. if (isResponse(result)) {
  2135. return result;
  2136. }
  2137. // When returning StaticHandlerContext, we patch back in the location here
  2138. // since we need it for React Context. But this helps keep our submit and
  2139. // loadRouteData operating on a Request instead of a Location
  2140. return { location, basename, ...result };
  2141. }
  2142. /**
  2143. * The queryRoute() method is intended for targeted route requests, either
  2144. * for fetch ?_data requests or resource route requests. In this case, we
  2145. * are only ever calling a single action or loader, and we are returning the
  2146. * returned value directly. In most cases, this will be a Response returned
  2147. * from the action/loader, but it may be a primitive or other value as well -
  2148. * and in such cases the calling context should handle that accordingly.
  2149. *
  2150. * We do respect the throw/return differentiation, so if an action/loader
  2151. * throws, then this method will throw the value. This is important so we
  2152. * can do proper boundary identification in Remix where a thrown Response
  2153. * must go to the Catch Boundary but a returned Response is happy-path.
  2154. *
  2155. * One thing to note is that any Router-initiated Errors that make sense
  2156. * to associate with a status code will be thrown as an ErrorResponse
  2157. * instance which include the raw Error, such that the calling context can
  2158. * serialize the error as they see fit while including the proper response
  2159. * code. Examples here are 404 and 405 errors that occur prior to reaching
  2160. * any user-defined loaders.
  2161. */
  2162. async function queryRoute(
  2163. request: Request,
  2164. {
  2165. routeId,
  2166. requestContext,
  2167. }: { requestContext?: unknown; routeId?: string } = {}
  2168. ): Promise<any> {
  2169. let url = new URL(request.url);
  2170. let method = request.method.toLowerCase();
  2171. let location = createLocation("", createPath(url), null, "default");
  2172. let matches = matchRoutes(dataRoutes, location, basename);
  2173. // SSR supports HEAD requests while SPA doesn't
  2174. if (!isValidMethod(method) && method !== "head" && method !== "options") {
  2175. throw getInternalRouterError(405, { method });
  2176. } else if (!matches) {
  2177. throw getInternalRouterError(404, { pathname: location.pathname });
  2178. }
  2179. let match = routeId
  2180. ? matches.find((m) => m.route.id === routeId)
  2181. : getTargetMatch(matches, location);
  2182. if (routeId && !match) {
  2183. throw getInternalRouterError(403, {
  2184. pathname: location.pathname,
  2185. routeId,
  2186. });
  2187. } else if (!match) {
  2188. // This should never hit I don't think?
  2189. throw getInternalRouterError(404, { pathname: location.pathname });
  2190. }
  2191. let result = await queryImpl(
  2192. request,
  2193. location,
  2194. matches,
  2195. requestContext,
  2196. match
  2197. );
  2198. if (isResponse(result)) {
  2199. return result;
  2200. }
  2201. let error = result.errors ? Object.values(result.errors)[0] : undefined;
  2202. if (error !== undefined) {
  2203. // If we got back result.errors, that means the loader/action threw
  2204. // _something_ that wasn't a Response, but it's not guaranteed/required
  2205. // to be an `instanceof Error` either, so we have to use throw here to
  2206. // preserve the "error" state outside of queryImpl.
  2207. throw error;
  2208. }
  2209. // Pick off the right state value to return
  2210. if (result.actionData) {
  2211. return Object.values(result.actionData)[0];
  2212. }
  2213. if (result.loaderData) {
  2214. let data = Object.values(result.loaderData)[0];
  2215. if (result.activeDeferreds?.[match.route.id]) {
  2216. data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
  2217. }
  2218. return data;
  2219. }
  2220. return undefined;
  2221. }
  2222. async function queryImpl(
  2223. request: Request,
  2224. location: Location,
  2225. matches: AgnosticDataRouteMatch[],
  2226. requestContext: unknown,
  2227. routeMatch?: AgnosticDataRouteMatch
  2228. ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
  2229. invariant(
  2230. request.signal,
  2231. "query()/queryRoute() requests must contain an AbortController signal"
  2232. );
  2233. try {
  2234. if (isMutationMethod(request.method.toLowerCase())) {
  2235. let result = await submit(
  2236. request,
  2237. matches,
  2238. routeMatch || getTargetMatch(matches, location),
  2239. requestContext,
  2240. routeMatch != null
  2241. );
  2242. return result;
  2243. }
  2244. let result = await loadRouteData(
  2245. request,
  2246. matches,
  2247. requestContext,
  2248. routeMatch
  2249. );
  2250. return isResponse(result)
  2251. ? result
  2252. : {
  2253. ...result,
  2254. actionData: null,
  2255. actionHeaders: {},
  2256. };
  2257. } catch (e) {
  2258. // If the user threw/returned a Response in callLoaderOrAction, we throw
  2259. // it to bail out and then return or throw here based on whether the user
  2260. // returned or threw
  2261. if (isQueryRouteResponse(e)) {
  2262. if (e.type === ResultType.error && !isRedirectResponse(e.response)) {
  2263. throw e.response;
  2264. }
  2265. return e.response;
  2266. }
  2267. // Redirects are always returned since they don't propagate to catch
  2268. // boundaries
  2269. if (isRedirectResponse(e)) {
  2270. return e;
  2271. }
  2272. throw e;
  2273. }
  2274. }
  2275. async function submit(
  2276. request: Request,
  2277. matches: AgnosticDataRouteMatch[],
  2278. actionMatch: AgnosticDataRouteMatch,
  2279. requestContext: unknown,
  2280. isRouteRequest: boolean
  2281. ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
  2282. let result: DataResult;
  2283. if (!actionMatch.route.action) {
  2284. let error = getInternalRouterError(405, {
  2285. method: request.method,
  2286. pathname: new URL(request.url).pathname,
  2287. routeId: actionMatch.route.id,
  2288. });
  2289. if (isRouteRequest) {
  2290. throw error;
  2291. }
  2292. result = {
  2293. type: ResultType.error,
  2294. error,
  2295. };
  2296. } else {
  2297. result = await callLoaderOrAction(
  2298. "action",
  2299. request,
  2300. actionMatch,
  2301. matches,
  2302. basename,
  2303. true,
  2304. isRouteRequest,
  2305. requestContext
  2306. );
  2307. if (request.signal.aborted) {
  2308. let method = isRouteRequest ? "queryRoute" : "query";
  2309. throw new Error(`${method}() call aborted`);
  2310. }
  2311. }
  2312. if (isRedirectResult(result)) {
  2313. // Uhhhh - this should never happen, we should always throw these from
  2314. // callLoaderOrAction, but the type narrowing here keeps TS happy and we
  2315. // can get back on the "throw all redirect responses" train here should
  2316. // this ever happen :/
  2317. throw new Response(null, {
  2318. status: result.status,
  2319. headers: {
  2320. Location: result.location,
  2321. },
  2322. });
  2323. }
  2324. if (isDeferredResult(result)) {
  2325. let error = getInternalRouterError(400, { type: "defer-action" });
  2326. if (isRouteRequest) {
  2327. throw error;
  2328. }
  2329. result = {
  2330. type: ResultType.error,
  2331. error,
  2332. };
  2333. }
  2334. if (isRouteRequest) {
  2335. // Note: This should only be non-Response values if we get here, since
  2336. // isRouteRequest should throw any Response received in callLoaderOrAction
  2337. if (isErrorResult(result)) {
  2338. throw result.error;
  2339. }
  2340. return {
  2341. matches: [actionMatch],
  2342. loaderData: {},
  2343. actionData: { [actionMatch.route.id]: result.data },
  2344. errors: null,
  2345. // Note: statusCode + headers are unused here since queryRoute will
  2346. // return the raw Response or value
  2347. statusCode: 200,
  2348. loaderHeaders: {},
  2349. actionHeaders: {},
  2350. activeDeferreds: null,
  2351. };
  2352. }
  2353. if (isErrorResult(result)) {
  2354. // Store off the pending error - we use it to determine which loaders
  2355. // to call and will commit it when we complete the navigation
  2356. let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
  2357. let context = await loadRouteData(
  2358. request,
  2359. matches,
  2360. requestContext,
  2361. undefined,
  2362. {
  2363. [boundaryMatch.route.id]: result.error,
  2364. }
  2365. );
  2366. // action status codes take precedence over loader status codes
  2367. return {
  2368. ...context,
  2369. statusCode: isRouteErrorResponse(result.error)
  2370. ? result.error.status
  2371. : 500,
  2372. actionData: null,
  2373. actionHeaders: {
  2374. ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
  2375. },
  2376. };
  2377. }
  2378. // Create a GET request for the loaders
  2379. let loaderRequest = new Request(request.url, {
  2380. headers: request.headers,
  2381. redirect: request.redirect,
  2382. signal: request.signal,
  2383. });
  2384. let context = await loadRouteData(loaderRequest, matches, requestContext);
  2385. return {
  2386. ...context,
  2387. // action status codes take precedence over loader status codes
  2388. ...(result.statusCode ? { statusCode: result.statusCode } : {}),
  2389. actionData: {
  2390. [actionMatch.route.id]: result.data,
  2391. },
  2392. actionHeaders: {
  2393. ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
  2394. },
  2395. };
  2396. }
  2397. async function loadRouteData(
  2398. request: Request,
  2399. matches: AgnosticDataRouteMatch[],
  2400. requestContext: unknown,
  2401. routeMatch?: AgnosticDataRouteMatch,
  2402. pendingActionError?: RouteData
  2403. ): Promise<
  2404. | Omit<
  2405. StaticHandlerContext,
  2406. "location" | "basename" | "actionData" | "actionHeaders"
  2407. >
  2408. | Response
  2409. > {
  2410. let isRouteRequest = routeMatch != null;
  2411. // Short circuit if we have no loaders to run (queryRoute())
  2412. if (isRouteRequest && !routeMatch?.route.loader) {
  2413. throw getInternalRouterError(400, {
  2414. method: request.method,
  2415. pathname: new URL(request.url).pathname,
  2416. routeId: routeMatch?.route.id,
  2417. });
  2418. }
  2419. let requestMatches = routeMatch
  2420. ? [routeMatch]
  2421. : getLoaderMatchesUntilBoundary(
  2422. matches,
  2423. Object.keys(pendingActionError || {})[0]
  2424. );
  2425. let matchesToLoad = requestMatches.filter((m) => m.route.loader);
  2426. // Short circuit if we have no loaders to run (query())
  2427. if (matchesToLoad.length === 0) {
  2428. return {
  2429. matches,
  2430. // Add a null for all matched routes for proper revalidation on the client
  2431. loaderData: matches.reduce(
  2432. (acc, m) => Object.assign(acc, { [m.route.id]: null }),
  2433. {}
  2434. ),
  2435. errors: pendingActionError || null,
  2436. statusCode: 200,
  2437. loaderHeaders: {},
  2438. activeDeferreds: null,
  2439. };
  2440. }
  2441. let results = await Promise.all([
  2442. ...matchesToLoad.map((match) =>
  2443. callLoaderOrAction(
  2444. "loader",
  2445. request,
  2446. match,
  2447. matches,
  2448. basename,
  2449. true,
  2450. isRouteRequest,
  2451. requestContext
  2452. )
  2453. ),
  2454. ]);
  2455. if (request.signal.aborted) {
  2456. let method = isRouteRequest ? "queryRoute" : "query";
  2457. throw new Error(`${method}() call aborted`);
  2458. }
  2459. // Process and commit output from loaders
  2460. let activeDeferreds = new Map<string, DeferredData>();
  2461. let context = processRouteLoaderData(
  2462. matches,
  2463. matchesToLoad,
  2464. results,
  2465. pendingActionError,
  2466. activeDeferreds
  2467. );
  2468. // Add a null for any non-loader matches for proper revalidation on the client
  2469. let executedLoaders = new Set<string>(
  2470. matchesToLoad.map((match) => match.route.id)
  2471. );
  2472. matches.forEach((match) => {
  2473. if (!executedLoaders.has(match.route.id)) {
  2474. context.loaderData[match.route.id] = null;
  2475. }
  2476. });
  2477. return {
  2478. ...context,
  2479. matches,
  2480. activeDeferreds:
  2481. activeDeferreds.size > 0
  2482. ? Object.fromEntries(activeDeferreds.entries())
  2483. : null,
  2484. };
  2485. }
  2486. return {
  2487. dataRoutes,
  2488. query,
  2489. queryRoute,
  2490. };
  2491. }
  2492. //#endregion
  2493. ////////////////////////////////////////////////////////////////////////////////
  2494. //#region Helpers
  2495. ////////////////////////////////////////////////////////////////////////////////
  2496. /**
  2497. * Given an existing StaticHandlerContext and an error thrown at render time,
  2498. * provide an updated StaticHandlerContext suitable for a second SSR render
  2499. */
  2500. export function getStaticContextFromError(
  2501. routes: AgnosticDataRouteObject[],
  2502. context: StaticHandlerContext,
  2503. error: any
  2504. ) {
  2505. let newContext: StaticHandlerContext = {
  2506. ...context,
  2507. statusCode: 500,
  2508. errors: {
  2509. [context._deepestRenderedBoundaryId || routes[0].id]: error,
  2510. },
  2511. };
  2512. return newContext;
  2513. }
  2514. function isSubmissionNavigation(
  2515. opts: RouterNavigateOptions
  2516. ): opts is SubmissionNavigateOptions {
  2517. return opts != null && "formData" in opts;
  2518. }
  2519. // Normalize navigation options by converting formMethod=GET formData objects to
  2520. // URLSearchParams so they behave identically to links with query params
  2521. function normalizeNavigateOptions(
  2522. to: To,
  2523. opts?: RouterNavigateOptions,
  2524. isFetcher = false
  2525. ): {
  2526. path: string;
  2527. submission?: Submission;
  2528. error?: ErrorResponse;
  2529. } {
  2530. let path = typeof to === "string" ? to : createPath(to);
  2531. // Return location verbatim on non-submission navigations
  2532. if (!opts || !isSubmissionNavigation(opts)) {
  2533. return { path };
  2534. }
  2535. if (opts.formMethod && !isValidMethod(opts.formMethod)) {
  2536. return {
  2537. path,
  2538. error: getInternalRouterError(405, { method: opts.formMethod }),
  2539. };
  2540. }
  2541. // Create a Submission on non-GET navigations
  2542. let submission: Submission | undefined;
  2543. if (opts.formData) {
  2544. submission = {
  2545. formMethod: opts.formMethod || "get",
  2546. formAction: stripHashFromPath(path),
  2547. formEncType:
  2548. (opts && opts.formEncType) || "application/x-www-form-urlencoded",
  2549. formData: opts.formData,
  2550. };
  2551. if (isMutationMethod(submission.formMethod)) {
  2552. return { path, submission };
  2553. }
  2554. }
  2555. // Flatten submission onto URLSearchParams for GET submissions
  2556. let parsedPath = parsePath(path);
  2557. let searchParams = convertFormDataToSearchParams(opts.formData);
  2558. // Since fetcher GET submissions only run a single loader (as opposed to
  2559. // navigation GET submissions which run all loaders), we need to preserve
  2560. // any incoming ?index params
  2561. if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
  2562. searchParams.append("index", "");
  2563. }
  2564. parsedPath.search = `?${searchParams}`;
  2565. return { path: createPath(parsedPath), submission };
  2566. }
  2567. // Filter out all routes below any caught error as they aren't going to
  2568. // render so we don't need to load them
  2569. function getLoaderMatchesUntilBoundary(
  2570. matches: AgnosticDataRouteMatch[],
  2571. boundaryId?: string
  2572. ) {
  2573. let boundaryMatches = matches;
  2574. if (boundaryId) {
  2575. let index = matches.findIndex((m) => m.route.id === boundaryId);
  2576. if (index >= 0) {
  2577. boundaryMatches = matches.slice(0, index);
  2578. }
  2579. }
  2580. return boundaryMatches;
  2581. }
  2582. function getMatchesToLoad(
  2583. history: History,
  2584. state: RouterState,
  2585. matches: AgnosticDataRouteMatch[],
  2586. submission: Submission | undefined,
  2587. location: Location,
  2588. isRevalidationRequired: boolean,
  2589. cancelledDeferredRoutes: string[],
  2590. cancelledFetcherLoads: string[],
  2591. pendingActionData?: RouteData,
  2592. pendingError?: RouteData,
  2593. fetchLoadMatches?: Map<string, FetchLoadMatch>
  2594. ): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
  2595. let actionResult = pendingError
  2596. ? Object.values(pendingError)[0]
  2597. : pendingActionData
  2598. ? Object.values(pendingActionData)[0]
  2599. : undefined;
  2600. let currentUrl = history.createURL(state.location);
  2601. let nextUrl = history.createURL(location);
  2602. let defaultShouldRevalidate =
  2603. // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
  2604. isRevalidationRequired ||
  2605. // Clicked the same link, resubmitted a GET form
  2606. currentUrl.toString() === nextUrl.toString() ||
  2607. // Search params affect all loaders
  2608. currentUrl.search !== nextUrl.search;
  2609. // Pick navigation matches that are net-new or qualify for revalidation
  2610. let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
  2611. let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
  2612. let navigationMatches = boundaryMatches.filter((match, index) => {
  2613. if (match.route.loader == null) {
  2614. return false;
  2615. }
  2616. // Always call the loader on new route instances and pending defer cancellations
  2617. if (
  2618. isNewLoader(state.loaderData, state.matches[index], match) ||
  2619. cancelledDeferredRoutes.some((id) => id === match.route.id)
  2620. ) {
  2621. return true;
  2622. }
  2623. // This is the default implementation for when we revalidate. If the route
  2624. // provides it's own implementation, then we give them full control but
  2625. // provide this value so they can leverage it if needed after they check
  2626. // their own specific use cases
  2627. let currentRouteMatch = state.matches[index];
  2628. let nextRouteMatch = match;
  2629. return shouldRevalidateLoader(match, {
  2630. currentUrl,
  2631. currentParams: currentRouteMatch.params,
  2632. nextUrl,
  2633. nextParams: nextRouteMatch.params,
  2634. ...submission,
  2635. actionResult,
  2636. defaultShouldRevalidate:
  2637. defaultShouldRevalidate ||
  2638. isNewRouteInstance(currentRouteMatch, nextRouteMatch),
  2639. });
  2640. });
  2641. // Pick fetcher.loads that need to be revalidated
  2642. let revalidatingFetchers: RevalidatingFetcher[] = [];
  2643. fetchLoadMatches &&
  2644. fetchLoadMatches.forEach((f, key) => {
  2645. if (!matches.some((m) => m.route.id === f.routeId)) {
  2646. // This fetcher is not going to be present in the subsequent render so
  2647. // there's no need to revalidate it
  2648. return;
  2649. } else if (cancelledFetcherLoads.includes(key)) {
  2650. // This fetcher was cancelled from a prior action submission - force reload
  2651. revalidatingFetchers.push({ key, ...f });
  2652. } else {
  2653. // Revalidating fetchers are decoupled from the route matches since they
  2654. // hit a static href, so they _always_ check shouldRevalidate and the
  2655. // default is strictly if a revalidation is explicitly required (action
  2656. // submissions, useRevalidator, X-Remix-Revalidate).
  2657. let shouldRevalidate = shouldRevalidateLoader(f.match, {
  2658. currentUrl,
  2659. currentParams: state.matches[state.matches.length - 1].params,
  2660. nextUrl,
  2661. nextParams: matches[matches.length - 1].params,
  2662. ...submission,
  2663. actionResult,
  2664. defaultShouldRevalidate,
  2665. });
  2666. if (shouldRevalidate) {
  2667. revalidatingFetchers.push({ key, ...f });
  2668. }
  2669. }
  2670. });
  2671. return [navigationMatches, revalidatingFetchers];
  2672. }
  2673. function isNewLoader(
  2674. currentLoaderData: RouteData,
  2675. currentMatch: AgnosticDataRouteMatch,
  2676. match: AgnosticDataRouteMatch
  2677. ) {
  2678. let isNew =
  2679. // [a] -> [a, b]
  2680. !currentMatch ||
  2681. // [a, b] -> [a, c]
  2682. match.route.id !== currentMatch.route.id;
  2683. // Handle the case that we don't have data for a re-used route, potentially
  2684. // from a prior error or from a cancelled pending deferred
  2685. let isMissingData = currentLoaderData[match.route.id] === undefined;
  2686. // Always load if this is a net-new route or we don't yet have data
  2687. return isNew || isMissingData;
  2688. }
  2689. function isNewRouteInstance(
  2690. currentMatch: AgnosticDataRouteMatch,
  2691. match: AgnosticDataRouteMatch
  2692. ) {
  2693. let currentPath = currentMatch.route.path;
  2694. return (
  2695. // param change for this match, /users/123 -> /users/456
  2696. currentMatch.pathname !== match.pathname ||
  2697. // splat param changed, which is not present in match.path
  2698. // e.g. /files/images/avatar.jpg -> files/finances.xls
  2699. (currentPath != null &&
  2700. currentPath.endsWith("*") &&
  2701. currentMatch.params["*"] !== match.params["*"])
  2702. );
  2703. }
  2704. function shouldRevalidateLoader(
  2705. loaderMatch: AgnosticDataRouteMatch,
  2706. arg: Parameters<ShouldRevalidateFunction>[0]
  2707. ) {
  2708. if (loaderMatch.route.shouldRevalidate) {
  2709. let routeChoice = loaderMatch.route.shouldRevalidate(arg);
  2710. if (typeof routeChoice === "boolean") {
  2711. return routeChoice;
  2712. }
  2713. }
  2714. return arg.defaultShouldRevalidate;
  2715. }
  2716. async function callLoaderOrAction(
  2717. type: "loader" | "action",
  2718. request: Request,
  2719. match: AgnosticDataRouteMatch,
  2720. matches: AgnosticDataRouteMatch[],
  2721. basename = "/",
  2722. isStaticRequest: boolean = false,
  2723. isRouteRequest: boolean = false,
  2724. requestContext?: unknown
  2725. ): Promise<DataResult> {
  2726. let resultType;
  2727. let result;
  2728. // Setup a promise we can race against so that abort signals short circuit
  2729. let reject: () => void;
  2730. let abortPromise = new Promise((_, r) => (reject = r));
  2731. let onReject = () => reject();
  2732. request.signal.addEventListener("abort", onReject);
  2733. try {
  2734. let handler = match.route[type];
  2735. invariant<Function>(
  2736. handler,
  2737. `Could not find the ${type} to run on the "${match.route.id}" route`
  2738. );
  2739. result = await Promise.race([
  2740. handler({ request, params: match.params, context: requestContext }),
  2741. abortPromise,
  2742. ]);
  2743. invariant(
  2744. result !== undefined,
  2745. `You defined ${type === "action" ? "an action" : "a loader"} for route ` +
  2746. `"${match.route.id}" but didn't return anything from your \`${type}\` ` +
  2747. `function. Please return a value or \`null\`.`
  2748. );
  2749. } catch (e) {
  2750. resultType = ResultType.error;
  2751. result = e;
  2752. } finally {
  2753. request.signal.removeEventListener("abort", onReject);
  2754. }
  2755. if (isResponse(result)) {
  2756. let status = result.status;
  2757. // Process redirects
  2758. if (redirectStatusCodes.has(status)) {
  2759. let location = result.headers.get("Location");
  2760. invariant(
  2761. location,
  2762. "Redirects returned/thrown from loaders/actions must have a Location header"
  2763. );
  2764. // Support relative routing in internal redirects
  2765. if (!ABSOLUTE_URL_REGEX.test(location)) {
  2766. let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
  2767. let routePathnames = getPathContributingMatches(activeMatches).map(
  2768. (match) => match.pathnameBase
  2769. );
  2770. let resolvedLocation = resolveTo(
  2771. location,
  2772. routePathnames,
  2773. new URL(request.url).pathname
  2774. );
  2775. invariant(
  2776. createPath(resolvedLocation),
  2777. `Unable to resolve redirect location: ${location}`
  2778. );
  2779. // Prepend the basename to the redirect location if we have one
  2780. if (basename) {
  2781. let path = resolvedLocation.pathname;
  2782. resolvedLocation.pathname =
  2783. path === "/" ? basename : joinPaths([basename, path]);
  2784. }
  2785. location = createPath(resolvedLocation);
  2786. } else if (!isStaticRequest) {
  2787. // Strip off the protocol+origin for same-origin absolute redirects.
  2788. // If this is a static reques, we can let it go back to the browser
  2789. // as-is
  2790. let currentUrl = new URL(request.url);
  2791. let url = location.startsWith("//")
  2792. ? new URL(currentUrl.protocol + location)
  2793. : new URL(location);
  2794. if (url.origin === currentUrl.origin) {
  2795. location = url.pathname + url.search + url.hash;
  2796. }
  2797. }
  2798. // Don't process redirects in the router during static requests requests.
  2799. // Instead, throw the Response and let the server handle it with an HTTP
  2800. // redirect. We also update the Location header in place in this flow so
  2801. // basename and relative routing is taken into account
  2802. if (isStaticRequest) {
  2803. result.headers.set("Location", location);
  2804. throw result;
  2805. }
  2806. return {
  2807. type: ResultType.redirect,
  2808. status,
  2809. location,
  2810. revalidate: result.headers.get("X-Remix-Revalidate") !== null,
  2811. };
  2812. }
  2813. // For SSR single-route requests, we want to hand Responses back directly
  2814. // without unwrapping. We do this with the QueryRouteResponse wrapper
  2815. // interface so we can know whether it was returned or thrown
  2816. if (isRouteRequest) {
  2817. // eslint-disable-next-line no-throw-literal
  2818. throw {
  2819. type: resultType || ResultType.data,
  2820. response: result,
  2821. };
  2822. }
  2823. let data: any;
  2824. let contentType = result.headers.get("Content-Type");
  2825. // Check between word boundaries instead of startsWith() due to the last
  2826. // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
  2827. if (contentType && /\bapplication\/json\b/.test(contentType)) {
  2828. data = await result.json();
  2829. } else {
  2830. data = await result.text();
  2831. }
  2832. if (resultType === ResultType.error) {
  2833. return {
  2834. type: resultType,
  2835. error: new ErrorResponse(status, result.statusText, data),
  2836. headers: result.headers,
  2837. };
  2838. }
  2839. return {
  2840. type: ResultType.data,
  2841. data,
  2842. statusCode: result.status,
  2843. headers: result.headers,
  2844. };
  2845. }
  2846. if (resultType === ResultType.error) {
  2847. return { type: resultType, error: result };
  2848. }
  2849. if (result instanceof DeferredData) {
  2850. return { type: ResultType.deferred, deferredData: result };
  2851. }
  2852. return { type: ResultType.data, data: result };
  2853. }
  2854. // Utility method for creating the Request instances for loaders/actions during
  2855. // client-side navigations and fetches. During SSR we will always have a
  2856. // Request instance from the static handler (query/queryRoute)
  2857. function createClientSideRequest(
  2858. history: History,
  2859. location: string | Location,
  2860. signal: AbortSignal,
  2861. submission?: Submission
  2862. ): Request {
  2863. let url = history.createURL(stripHashFromPath(location)).toString();
  2864. let init: RequestInit = { signal };
  2865. if (submission && isMutationMethod(submission.formMethod)) {
  2866. let { formMethod, formEncType, formData } = submission;
  2867. init.method = formMethod.toUpperCase();
  2868. init.body =
  2869. formEncType === "application/x-www-form-urlencoded"
  2870. ? convertFormDataToSearchParams(formData)
  2871. : formData;
  2872. }
  2873. // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
  2874. return new Request(url, init);
  2875. }
  2876. function convertFormDataToSearchParams(formData: FormData): URLSearchParams {
  2877. let searchParams = new URLSearchParams();
  2878. for (let [key, value] of formData.entries()) {
  2879. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
  2880. searchParams.append(key, value instanceof File ? value.name : value);
  2881. }
  2882. return searchParams;
  2883. }
  2884. function processRouteLoaderData(
  2885. matches: AgnosticDataRouteMatch[],
  2886. matchesToLoad: AgnosticDataRouteMatch[],
  2887. results: DataResult[],
  2888. pendingError: RouteData | undefined,
  2889. activeDeferreds: Map<string, DeferredData>
  2890. ): {
  2891. loaderData: RouterState["loaderData"];
  2892. errors: RouterState["errors"] | null;
  2893. statusCode: number;
  2894. loaderHeaders: Record<string, Headers>;
  2895. } {
  2896. // Fill in loaderData/errors from our loaders
  2897. let loaderData: RouterState["loaderData"] = {};
  2898. let errors: RouterState["errors"] | null = null;
  2899. let statusCode: number | undefined;
  2900. let foundError = false;
  2901. let loaderHeaders: Record<string, Headers> = {};
  2902. // Process loader results into state.loaderData/state.errors
  2903. results.forEach((result, index) => {
  2904. let id = matchesToLoad[index].route.id;
  2905. invariant(
  2906. !isRedirectResult(result),
  2907. "Cannot handle redirect results in processLoaderData"
  2908. );
  2909. if (isErrorResult(result)) {
  2910. // Look upwards from the matched route for the closest ancestor
  2911. // error boundary, defaulting to the root match
  2912. let boundaryMatch = findNearestBoundary(matches, id);
  2913. let error = result.error;
  2914. // If we have a pending action error, we report it at the highest-route
  2915. // that throws a loader error, and then clear it out to indicate that
  2916. // it was consumed
  2917. if (pendingError) {
  2918. error = Object.values(pendingError)[0];
  2919. pendingError = undefined;
  2920. }
  2921. errors = errors || {};
  2922. // Prefer higher error values if lower errors bubble to the same boundary
  2923. if (errors[boundaryMatch.route.id] == null) {
  2924. errors[boundaryMatch.route.id] = error;
  2925. }
  2926. // Clear our any prior loaderData for the throwing route
  2927. loaderData[id] = undefined;
  2928. // Once we find our first (highest) error, we set the status code and
  2929. // prevent deeper status codes from overriding
  2930. if (!foundError) {
  2931. foundError = true;
  2932. statusCode = isRouteErrorResponse(result.error)
  2933. ? result.error.status
  2934. : 500;
  2935. }
  2936. if (result.headers) {
  2937. loaderHeaders[id] = result.headers;
  2938. }
  2939. } else {
  2940. if (isDeferredResult(result)) {
  2941. activeDeferreds.set(id, result.deferredData);
  2942. loaderData[id] = result.deferredData.data;
  2943. } else {
  2944. loaderData[id] = result.data;
  2945. }
  2946. // Error status codes always override success status codes, but if all
  2947. // loaders are successful we take the deepest status code.
  2948. if (
  2949. result.statusCode != null &&
  2950. result.statusCode !== 200 &&
  2951. !foundError
  2952. ) {
  2953. statusCode = result.statusCode;
  2954. }
  2955. if (result.headers) {
  2956. loaderHeaders[id] = result.headers;
  2957. }
  2958. }
  2959. });
  2960. // If we didn't consume the pending action error (i.e., all loaders
  2961. // resolved), then consume it here. Also clear out any loaderData for the
  2962. // throwing route
  2963. if (pendingError) {
  2964. errors = pendingError;
  2965. loaderData[Object.keys(pendingError)[0]] = undefined;
  2966. }
  2967. return {
  2968. loaderData,
  2969. errors,
  2970. statusCode: statusCode || 200,
  2971. loaderHeaders,
  2972. };
  2973. }
  2974. function processLoaderData(
  2975. state: RouterState,
  2976. matches: AgnosticDataRouteMatch[],
  2977. matchesToLoad: AgnosticDataRouteMatch[],
  2978. results: DataResult[],
  2979. pendingError: RouteData | undefined,
  2980. revalidatingFetchers: RevalidatingFetcher[],
  2981. fetcherResults: DataResult[],
  2982. activeDeferreds: Map<string, DeferredData>
  2983. ): {
  2984. loaderData: RouterState["loaderData"];
  2985. errors?: RouterState["errors"];
  2986. } {
  2987. let { loaderData, errors } = processRouteLoaderData(
  2988. matches,
  2989. matchesToLoad,
  2990. results,
  2991. pendingError,
  2992. activeDeferreds
  2993. );
  2994. // Process results from our revalidating fetchers
  2995. for (let index = 0; index < revalidatingFetchers.length; index++) {
  2996. let { key, match } = revalidatingFetchers[index];
  2997. invariant(
  2998. fetcherResults !== undefined && fetcherResults[index] !== undefined,
  2999. "Did not find corresponding fetcher result"
  3000. );
  3001. let result = fetcherResults[index];
  3002. // Process fetcher non-redirect errors
  3003. if (isErrorResult(result)) {
  3004. let boundaryMatch = findNearestBoundary(state.matches, match.route.id);
  3005. if (!(errors && errors[boundaryMatch.route.id])) {
  3006. errors = {
  3007. ...errors,
  3008. [boundaryMatch.route.id]: result.error,
  3009. };
  3010. }
  3011. state.fetchers.delete(key);
  3012. } else if (isRedirectResult(result)) {
  3013. // Should never get here, redirects should get processed above, but we
  3014. // keep this to type narrow to a success result in the else
  3015. invariant(false, "Unhandled fetcher revalidation redirect");
  3016. } else if (isDeferredResult(result)) {
  3017. // Should never get here, deferred data should be awaited for fetchers
  3018. // in resolveDeferredResults
  3019. invariant(false, "Unhandled fetcher deferred data");
  3020. } else {
  3021. let doneFetcher: FetcherStates["Idle"] = {
  3022. state: "idle",
  3023. data: result.data,
  3024. formMethod: undefined,
  3025. formAction: undefined,
  3026. formEncType: undefined,
  3027. formData: undefined,
  3028. " _hasFetcherDoneAnything ": true,
  3029. };
  3030. state.fetchers.set(key, doneFetcher);
  3031. }
  3032. }
  3033. return { loaderData, errors };
  3034. }
  3035. function mergeLoaderData(
  3036. loaderData: RouteData,
  3037. newLoaderData: RouteData,
  3038. matches: AgnosticDataRouteMatch[],
  3039. errors: RouteData | null | undefined
  3040. ): RouteData {
  3041. let mergedLoaderData = { ...newLoaderData };
  3042. for (let match of matches) {
  3043. let id = match.route.id;
  3044. if (newLoaderData.hasOwnProperty(id)) {
  3045. if (newLoaderData[id] !== undefined) {
  3046. mergedLoaderData[id] = newLoaderData[id];
  3047. } else {
  3048. // No-op - this is so we ignore existing data if we have a key in the
  3049. // incoming object with an undefined value, which is how we unset a prior
  3050. // loaderData if we encounter a loader error
  3051. }
  3052. } else if (loaderData[id] !== undefined) {
  3053. mergedLoaderData[id] = loaderData[id];
  3054. }
  3055. if (errors && errors.hasOwnProperty(id)) {
  3056. // Don't keep any loader data below the boundary
  3057. break;
  3058. }
  3059. }
  3060. return mergedLoaderData;
  3061. }
  3062. // Find the nearest error boundary, looking upwards from the leaf route (or the
  3063. // route specified by routeId) for the closest ancestor error boundary,
  3064. // defaulting to the root match
  3065. function findNearestBoundary(
  3066. matches: AgnosticDataRouteMatch[],
  3067. routeId?: string
  3068. ): AgnosticDataRouteMatch {
  3069. let eligibleMatches = routeId
  3070. ? matches.slice(0, matches.findIndex((m) => m.route.id === routeId) + 1)
  3071. : [...matches];
  3072. return (
  3073. eligibleMatches.reverse().find((m) => m.route.hasErrorBoundary === true) ||
  3074. matches[0]
  3075. );
  3076. }
  3077. function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {
  3078. matches: AgnosticDataRouteMatch[];
  3079. route: AgnosticDataRouteObject;
  3080. } {
  3081. // Prefer a root layout route if present, otherwise shim in a route object
  3082. let route = routes.find((r) => r.index || !r.path || r.path === "/") || {
  3083. id: `__shim-error-route__`,
  3084. };
  3085. return {
  3086. matches: [
  3087. {
  3088. params: {},
  3089. pathname: "",
  3090. pathnameBase: "",
  3091. route,
  3092. },
  3093. ],
  3094. route,
  3095. };
  3096. }
  3097. function getInternalRouterError(
  3098. status: number,
  3099. {
  3100. pathname,
  3101. routeId,
  3102. method,
  3103. type,
  3104. }: {
  3105. pathname?: string;
  3106. routeId?: string;
  3107. method?: string;
  3108. type?: "defer-action";
  3109. } = {}
  3110. ) {
  3111. let statusText = "Unknown Server Error";
  3112. let errorMessage = "Unknown @remix-run/router error";
  3113. if (status === 400) {
  3114. statusText = "Bad Request";
  3115. if (method && pathname && routeId) {
  3116. errorMessage =
  3117. `You made a ${method} request to "${pathname}" but ` +
  3118. `did not provide a \`loader\` for route "${routeId}", ` +
  3119. `so there is no way to handle the request.`;
  3120. } else if (type === "defer-action") {
  3121. errorMessage = "defer() is not supported in actions";
  3122. }
  3123. } else if (status === 403) {
  3124. statusText = "Forbidden";
  3125. errorMessage = `Route "${routeId}" does not match URL "${pathname}"`;
  3126. } else if (status === 404) {
  3127. statusText = "Not Found";
  3128. errorMessage = `No route matches URL "${pathname}"`;
  3129. } else if (status === 405) {
  3130. statusText = "Method Not Allowed";
  3131. if (method && pathname && routeId) {
  3132. errorMessage =
  3133. `You made a ${method.toUpperCase()} request to "${pathname}" but ` +
  3134. `did not provide an \`action\` for route "${routeId}", ` +
  3135. `so there is no way to handle the request.`;
  3136. } else if (method) {
  3137. errorMessage = `Invalid request method "${method.toUpperCase()}"`;
  3138. }
  3139. }
  3140. return new ErrorResponse(
  3141. status || 500,
  3142. statusText,
  3143. new Error(errorMessage),
  3144. true
  3145. );
  3146. }
  3147. // Find any returned redirect errors, starting from the lowest match
  3148. function findRedirect(results: DataResult[]): RedirectResult | undefined {
  3149. for (let i = results.length - 1; i >= 0; i--) {
  3150. let result = results[i];
  3151. if (isRedirectResult(result)) {
  3152. return result;
  3153. }
  3154. }
  3155. }
  3156. function stripHashFromPath(path: To) {
  3157. let parsedPath = typeof path === "string" ? parsePath(path) : path;
  3158. return createPath({ ...parsedPath, hash: "" });
  3159. }
  3160. function isHashChangeOnly(a: Location, b: Location): boolean {
  3161. return (
  3162. a.pathname === b.pathname && a.search === b.search && a.hash !== b.hash
  3163. );
  3164. }
  3165. function isDeferredResult(result: DataResult): result is DeferredResult {
  3166. return result.type === ResultType.deferred;
  3167. }
  3168. function isErrorResult(result: DataResult): result is ErrorResult {
  3169. return result.type === ResultType.error;
  3170. }
  3171. function isRedirectResult(result?: DataResult): result is RedirectResult {
  3172. return (result && result.type) === ResultType.redirect;
  3173. }
  3174. function isResponse(value: any): value is Response {
  3175. return (
  3176. value != null &&
  3177. typeof value.status === "number" &&
  3178. typeof value.statusText === "string" &&
  3179. typeof value.headers === "object" &&
  3180. typeof value.body !== "undefined"
  3181. );
  3182. }
  3183. function isRedirectResponse(result: any): result is Response {
  3184. if (!isResponse(result)) {
  3185. return false;
  3186. }
  3187. let status = result.status;
  3188. let location = result.headers.get("Location");
  3189. return status >= 300 && status <= 399 && location != null;
  3190. }
  3191. function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
  3192. return (
  3193. obj &&
  3194. isResponse(obj.response) &&
  3195. (obj.type === ResultType.data || ResultType.error)
  3196. );
  3197. }
  3198. function isValidMethod(method: string): method is FormMethod {
  3199. return validRequestMethods.has(method as FormMethod);
  3200. }
  3201. function isMutationMethod(method?: string): method is MutationFormMethod {
  3202. return validMutationMethods.has(method as MutationFormMethod);
  3203. }
  3204. async function resolveDeferredResults(
  3205. currentMatches: AgnosticDataRouteMatch[],
  3206. matchesToLoad: AgnosticDataRouteMatch[],
  3207. results: DataResult[],
  3208. signal: AbortSignal,
  3209. isFetcher: boolean,
  3210. currentLoaderData?: RouteData
  3211. ) {
  3212. for (let index = 0; index < results.length; index++) {
  3213. let result = results[index];
  3214. let match = matchesToLoad[index];
  3215. let currentMatch = currentMatches.find(
  3216. (m) => m.route.id === match.route.id
  3217. );
  3218. let isRevalidatingLoader =
  3219. currentMatch != null &&
  3220. !isNewRouteInstance(currentMatch, match) &&
  3221. (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
  3222. if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) {
  3223. // Note: we do not have to touch activeDeferreds here since we race them
  3224. // against the signal in resolveDeferredData and they'll get aborted
  3225. // there if needed
  3226. await resolveDeferredData(result, signal, isFetcher).then((result) => {
  3227. if (result) {
  3228. results[index] = result || results[index];
  3229. }
  3230. });
  3231. }
  3232. }
  3233. }
  3234. async function resolveDeferredData(
  3235. result: DeferredResult,
  3236. signal: AbortSignal,
  3237. unwrap = false
  3238. ): Promise<SuccessResult | ErrorResult | undefined> {
  3239. let aborted = await result.deferredData.resolveData(signal);
  3240. if (aborted) {
  3241. return;
  3242. }
  3243. if (unwrap) {
  3244. try {
  3245. return {
  3246. type: ResultType.data,
  3247. data: result.deferredData.unwrappedData,
  3248. };
  3249. } catch (e) {
  3250. // Handle any TrackedPromise._error values encountered while unwrapping
  3251. return {
  3252. type: ResultType.error,
  3253. error: e,
  3254. };
  3255. }
  3256. }
  3257. return {
  3258. type: ResultType.data,
  3259. data: result.deferredData.data,
  3260. };
  3261. }
  3262. function hasNakedIndexQuery(search: string): boolean {
  3263. return new URLSearchParams(search).getAll("index").some((v) => v === "");
  3264. }
  3265. // Note: This should match the format exported by useMatches, so if you change
  3266. // this please also change that :) Eventually we'll DRY this up
  3267. function createUseMatchesMatch(
  3268. match: AgnosticDataRouteMatch,
  3269. loaderData: RouteData
  3270. ): UseMatchesMatch {
  3271. let { route, pathname, params } = match;
  3272. return {
  3273. id: route.id,
  3274. pathname,
  3275. params,
  3276. data: loaderData[route.id] as unknown,
  3277. handle: route.handle as unknown,
  3278. };
  3279. }
  3280. function getTargetMatch(
  3281. matches: AgnosticDataRouteMatch[],
  3282. location: Location | string
  3283. ) {
  3284. let search =
  3285. typeof location === "string" ? parsePath(location).search : location.search;
  3286. if (
  3287. matches[matches.length - 1].route.index &&
  3288. hasNakedIndexQuery(search || "")
  3289. ) {
  3290. // Return the leaf index route when index is present
  3291. return matches[matches.length - 1];
  3292. }
  3293. // Otherwise grab the deepest "path contributing" match (ignoring index and
  3294. // pathless layout routes)
  3295. let pathMatches = getPathContributingMatches(matches);
  3296. return pathMatches[pathMatches.length - 1];
  3297. }
  3298. //#endregion