@solidjs/router 0.13.5 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ A router lets you change your view based on the URL in the browser. This allows
10
10
 
11
11
  Solid Router is a universal router for SolidJS - it works whether you're rendering on the client or on the server. It was inspired by and combines paradigms of React Router and the Ember Router. Routes can be defined directly in your app's template using JSX, but you can also pass your route configuration directly as an object. It also supports nested routing, so navigation can change a part of a component, rather than completely replacing it.
12
12
 
13
- It supports all of Solid's SSR methods and has Solid's transitions baked in, so use it freely with suspense, resources, and lazy components. Solid Router also allows you to define a load function that loads parallel to the routes ([render-as-you-fetch](https://epicreact.dev/render-as-you-fetch/)).
13
+ It supports all of Solid's SSR methods and has Solid's transitions baked in, so use it freely with suspense, resources, and lazy components. Solid Router also allows you to define a preload function that loads parallel to the routes ([render-as-you-fetch](https://epicreact.dev/render-as-you-fetch/)).
14
14
 
15
15
  - [Getting Started](#getting-started)
16
16
  - [Set Up the Router](#set-up-the-router)
@@ -372,17 +372,19 @@ You can nest indefinitely - just remember that only leaf nodes will become their
372
372
  </div>
373
373
  }
374
374
  >
375
- <Route path="layer2"
376
- component={() => <div>Innermost layer</div>}> </Route>
375
+ <Route
376
+ path="layer2"
377
+ component={() => <div>Innermost layer</div>}
378
+ />
377
379
  </Route>
378
380
  </Route>
379
381
  ```
380
382
 
381
- ## Load Functions
383
+ ## Preload Functions
382
384
 
383
- Even with smart caches it is possible that we have waterfalls both with view logic and with lazy loaded code. With load functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible. The load function is called when the Route is loaded or eagerly when links are hovered.
385
+ Even with smart caches it is possible that we have waterfalls both with view logic and with lazy loaded code. With preload functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible. The preload function is called when the Route is loaded or eagerly when links are hovered.
384
386
 
385
- As its only argument, the load function is passed an object that you can use to access route information:
387
+ As its only argument, the preload function is passed an object that you can use to access route information:
386
388
 
387
389
  ```js
388
390
  import { lazy } from "solid-js";
@@ -390,13 +392,13 @@ import { Route } from "@solidjs/router";
390
392
 
391
393
  const User = lazy(() => import("./pages/users/[id].js"));
392
394
 
393
- // load function
394
- function loadUser({params, location}) {
395
- // do loading
395
+ // preload function
396
+ function preloadUser({params, location}) {
397
+ // do preloading
396
398
  }
397
399
 
398
400
  // Pass it in the route definition
399
- <Route path="/users/:id" component={User} load={loadUser} />;
401
+ <Route path="/users/:id" component={User} preload={preloadUser} />;
400
402
  ```
401
403
 
402
404
  | key | type | description |
@@ -406,24 +408,24 @@ function loadUser({params, location}) {
406
408
  | intent | `"initial", "navigate", "native", "preload"` | Indicates why this function is being called. <ul><li>"initial" - the route is being initially shown (ie page load)</li><li>"native" - navigate originated from the browser (eg back/forward)</li><li>"navigate" - navigate originated from the router (eg call to navigate or anchor clicked)</li><li>"preload" - not navigating, just preloading (eg link hover)</li></ul> |
407
409
 
408
410
 
409
- A common pattern is to export the load function and data wrappers that corresponds to a route in a dedicated `route.data.js` file. This way, the data function can be imported without loading anything else.
411
+ A common pattern is to export the preload function and data wrappers that corresponds to a route in a dedicated `route.data.js` file. This way, the data function can be imported without loading anything else.
410
412
 
411
413
  ```js
412
414
  import { lazy } from "solid-js";
413
415
  import { Route } from "@solidjs/router";
414
- import loadUser from "./pages/users/[id].data.js";
416
+ import preloadUser from "./pages/users/[id].data.js";
415
417
  const User = lazy(() => import("/pages/users/[id].js"));
416
418
 
417
419
  // In the Route definition
418
- <Route path="/users/:id" component={User} load={loadUser} />;
420
+ <Route path="/users/:id" component={User} preload={preloadUser} />;
419
421
  ```
420
422
 
421
- The return value of the `load` function is passed to the page component when called at anytime other than `"preload"`, so you can initialize things in there, or alternatively use our new Data APIs:
423
+ The return value of the `preload` function is passed to the page component when called at anytime other than `"preload"` intent, so you can initialize things in there, or alternatively use our new Data APIs:
422
424
 
423
425
 
424
426
  ## Data APIs
425
427
 
426
- Keep in mind these are completely optional. To use but showcase the power of our load mechanism.
428
+ Keep in mind these are completely optional. To use but showcase the power of our preload mechanism.
427
429
 
428
430
  ### `cache`
429
431
 
@@ -439,11 +441,11 @@ It is expected that the arguments to the cache function are serializable.
439
441
  This cache accomplishes the following:
440
442
 
441
443
  1. It does just deduping on the server for the lifetime of the request.
442
- 2. It does preload cache in the browser which lasts 10 seconds. When a route is preloaded on hover or when load is called when entering a route it will make sure to dedupe calls.
444
+ 2. It does preload cache in the browser which lasts 5 seconds. When a route is preloaded on hover or when preload is called when entering a route it will make sure to dedupe calls.
443
445
  3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
444
446
  4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses it. Revalidation or new fetch updates the cache.
445
447
 
446
- Using it with load function might look like:
448
+ Using it with preload function might look like:
447
449
 
448
450
  ```js
449
451
  import { lazy } from "solid-js";
@@ -452,13 +454,13 @@ import { getUser } from ... // the cache function
452
454
 
453
455
  const User = lazy(() => import("./pages/users/[id].js"));
454
456
 
455
- // load function
456
- function loadUser({params, location}) {
457
+ // preload function
458
+ function preloadUser({params, location}) {
457
459
  void getUser(params.id)
458
460
  }
459
461
 
460
462
  // Pass it in the route definition
461
- <Route path="/users/:id" component={User} load={loadUser} />;
463
+ <Route path="/users/:id" component={User} preload={preloadUser} />;
462
464
  ```
463
465
 
464
466
  Inside your page component you:
@@ -768,7 +770,7 @@ The Component for defining Routes:
768
770
  | component | `Component` | Component that will be rendered for the matched segment |
769
771
  | matchFilters | `MatchFilters` | Additional constraints for matching against the route |
770
772
  | children | `JSX.Element` | Nested `<Route>` definitions |
771
- | load | `RouteLoadFunc` | Function called during preload or when the route is navigated to. |
773
+ | preload | `RoutePreloadFunc` | Function called during preload or when the route is navigated to. |
772
774
 
773
775
  ## Router Primitives
774
776
 
@@ -871,6 +873,16 @@ const matches = useCurrentMatches();
871
873
  const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
872
874
  ```
873
875
 
876
+ ### usePreloadRoute
877
+
878
+ `usePreloadRoute` returns a function that can be used to preload a route manual. This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
879
+
880
+ ```js
881
+ const preload = usePreloadRoute();
882
+
883
+ preload(`/users/settings`, { preloadData: true });
884
+ ```
885
+
874
886
  ### useBeforeLeave
875
887
 
876
888
  `useBeforeLeave` takes a function that will be called prior to leaving a route. The function will be called with:
@@ -917,7 +929,7 @@ Related without Outlet component it has to be passed in manually. At which point
917
929
 
918
930
  ### `data` functions & `useRouteData`
919
931
 
920
- These have been replaced by a load mechanism. This allows link hover preloads (as the load function can be run as much as wanted without worry about reactivity). It support deduping/cache APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks.
932
+ These have been replaced by a preload mechanism. This allows link hover preloads (as the preload function can be run as much as wanted without worry about reactivity). It support deduping/cache APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks.
921
933
 
922
934
  That being said you can reproduce the old pattern largely by turning off preloads at the router level and then injecting your own Context:
923
935
 
@@ -927,15 +939,15 @@ import { Route } from "@solidjs/router";
927
939
 
928
940
  const User = lazy(() => import("./pages/users/[id].js"));
929
941
 
930
- // load function
931
- function loadUser({params, location}) {
942
+ // preload function
943
+ function preloadUser({params, location}) {
932
944
  const [user] = createResource(() => params.id, fetchUser);
933
945
  return user;
934
946
  }
935
947
 
936
948
  // Pass it in the route definition
937
949
  <Router preload={false}>
938
- <Route path="/users/:id" component={User} load={loadUser} />
950
+ <Route path="/users/:id" component={User} preload={preloadUser} />
939
951
  </Router>
940
952
  ```
941
953
 
@@ -1,13 +1,13 @@
1
1
  import { JSX } from "solid-js";
2
- import { Submission } from "../types.js";
3
- export type Action<T extends Array<any>, U> = (T extends [FormData] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<U>) & {
2
+ import type { Submission, SubmissionStub, NarrowResponse } from "../types.js";
3
+ export type Action<T extends Array<any>, U> = (T extends [FormData] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
4
4
  url: string;
5
- with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<U>, ...args: A): Action<B, U>;
5
+ with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<NarrowResponse<U>>, ...args: A): Action<B, U>;
6
6
  };
7
7
  export declare const actions: Map<string, Action<any, any>>;
8
8
  export declare function useSubmissions<T extends Array<any>, U>(fn: Action<T, U>, filter?: (arg: T) => boolean): Submission<T, U>[] & {
9
9
  pending: boolean;
10
10
  };
11
- export declare function useSubmission<T extends Array<any>, U>(fn: Action<T, U>, filter?: (arg: T) => boolean): Submission<T, U>;
12
- export declare function useAction<T extends Array<any>, U>(action: Action<T, U>): (...args: Parameters<Action<T, U>>) => Promise<U>;
11
+ export declare function useSubmission<T extends Array<any>, U>(fn: Action<T, U>, filter?: (arg: T) => boolean): Submission<T, U> | SubmissionStub;
12
+ export declare function useAction<T extends Array<any>, U>(action: Action<T, U>): (...args: Parameters<Action<T, U>>) => Promise<NarrowResponse<U>>;
13
13
  export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>;
@@ -21,6 +21,8 @@ export function useSubmission(fn, filter) {
21
21
  const submissions = useSubmissions(fn, filter);
22
22
  return new Proxy({}, {
23
23
  get(_, property) {
24
+ if (submissions.length === 0 && property === "clear" || property === "retry")
25
+ return (() => { });
24
26
  return submissions[submissions.length - 1]?.[property];
25
27
  }
26
28
  });
@@ -1,9 +1,9 @@
1
- import { CacheEntry } from "../types.js";
1
+ import type { CacheEntry, NarrowResponse } from "../types.js";
2
2
  export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
3
3
  export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
4
4
  export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends {
5
5
  [K in keyof A]-?: A[K];
6
- } ? (...args: never[]) => R : T) & {
6
+ } ? (...args: never[]) => R extends Promise<infer P> ? Promise<NarrowResponse<P>> : NarrowResponse<R> : (...args: A) => R extends Promise<infer P> ? Promise<NarrowResponse<P>> : NarrowResponse<R>) & {
7
7
  keyFor: (...args: A) => string;
8
8
  key: string;
9
9
  } : never;
@@ -1,6 +1,6 @@
1
1
  import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
2
2
  import { getRequestEvent, isServer } from "solid-js/web";
3
- import { useNavigate, getIntent, getInLoadFn } from "../routing.js";
3
+ import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
4
4
  const LocationHeader = "Location";
5
5
  const PRELOAD_TIMEOUT = 5000;
6
6
  const CACHE_TIMEOUT = 180000;
@@ -47,7 +47,7 @@ export function cache(fn, name) {
47
47
  const cachedFn = ((...args) => {
48
48
  const cache = getCache();
49
49
  const intent = getIntent();
50
- const inLoadFn = getInLoadFn();
50
+ const inPreloadFn = getInPreloadFn();
51
51
  const owner = getOwner();
52
52
  const navigate = owner ? useNavigate() : undefined;
53
53
  const now = Date.now();
@@ -87,15 +87,14 @@ export function cache(fn, name) {
87
87
  cached[0] = now;
88
88
  }
89
89
  let res = cached[1];
90
- if (!inLoadFn) {
90
+ if (intent !== "preload") {
91
91
  res =
92
92
  "then" in cached[1]
93
93
  ? cached[1].then(handleResponse(false), handleResponse(true))
94
94
  : handleResponse(false)(cached[1]);
95
95
  !isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
96
96
  }
97
- else
98
- "then" in res && res.catch(() => { });
97
+ inPreloadFn && "then" in res && res.catch(() => { });
99
98
  return res;
100
99
  }
101
100
  let res = !isServer && sharedConfig.context && sharedConfig.has(key)
@@ -120,14 +119,13 @@ export function cache(fn, name) {
120
119
  if (e && e.router.dataOnly)
121
120
  return (e.router.data[key] = res);
122
121
  }
123
- if (!inLoadFn) {
122
+ if (intent !== "preload") {
124
123
  res =
125
124
  "then" in res
126
125
  ? res.then(handleResponse(false), handleResponse(true))
127
126
  : handleResponse(false)(res);
128
127
  }
129
- else
130
- "then" in res && res.catch(() => { });
128
+ inPreloadFn && "then" in res && res.catch(() => { });
131
129
  // serialize on server
132
130
  if (isServer &&
133
131
  sharedConfig.context &&
@@ -140,19 +138,19 @@ export function cache(fn, name) {
140
138
  function handleResponse(error) {
141
139
  return async (v) => {
142
140
  if (v instanceof Response) {
143
- if (v.headers.has("Location")) {
144
- if (navigate) {
141
+ const url = v.headers.get(LocationHeader);
142
+ if (url !== null) {
143
+ // client + server relative redirect
144
+ if (navigate && url.startsWith("/"))
145
145
  startTransition(() => {
146
- let url = v.headers.get(LocationHeader);
147
- if (url && url.startsWith("/")) {
148
- navigate(url, {
149
- replace: true
150
- });
151
- }
152
- else if (!isServer && url) {
153
- window.location.href = url;
154
- }
146
+ navigate(url, { replace: true });
155
147
  });
148
+ else if (!isServer)
149
+ window.location.href = url;
150
+ else if (isServer) {
151
+ const e = getRequestEvent();
152
+ if (e)
153
+ e.response = { status: 302, headers: new Headers({ Location: url }) };
156
154
  }
157
155
  return;
158
156
  }
@@ -61,7 +61,7 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB
61
61
  url.pathname = transformUrl(url.pathname);
62
62
  }
63
63
  if (!preloadTimeout[url.pathname])
64
- router.preloadRoute(url, a.getAttribute("preload") !== "false");
64
+ router.preloadRoute(url, { preloadData: a.getAttribute("preload") !== "false" });
65
65
  }
66
66
  function handleAnchorIn(evt) {
67
67
  const res = handleAnchor(evt);
@@ -74,7 +74,7 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB
74
74
  if (preloadTimeout[url.pathname])
75
75
  return;
76
76
  preloadTimeout[url.pathname] = setTimeout(() => {
77
- router.preloadRoute(url, a.getAttribute("preload") !== "false");
77
+ router.preloadRoute(url, { preloadData: a.getAttribute("preload") !== "false" });
78
78
  delete preloadTimeout[url.pathname];
79
79
  }, 200);
80
80
  }
@@ -111,10 +111,10 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB
111
111
  const handler = actions.get(actionRef);
112
112
  if (handler) {
113
113
  evt.preventDefault();
114
- const data = new FormData(evt.target);
115
- if (evt.submitter && evt.submitter.name)
116
- data.append(evt.submitter.name, evt.submitter.value);
117
- handler.call({ r: router, f: evt.target }, data);
114
+ const data = new FormData(evt.target, evt.submitter);
115
+ handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
116
+ ? data
117
+ : new URLSearchParams(data));
118
118
  }
119
119
  }
120
120
  // ensure delegated event run first
@@ -1,6 +1,4 @@
1
- export type RouterResponseInit = Omit<ResponseInit, "body"> & {
2
- revalidate?: string | string[];
3
- };
4
- export declare function redirect(url: string, init?: number | RouterResponseInit): never;
5
- export declare function reload(init?: RouterResponseInit): never;
6
- export declare function json<T>(data: T, init?: RouterResponseInit): T;
1
+ import type { RouterResponseInit, CustomResponse } from "../types";
2
+ export declare function redirect(url: string, init?: number | RouterResponseInit): CustomResponse<never>;
3
+ export declare function reload(init?: RouterResponseInit): CustomResponse<never>;
4
+ export declare function json<T>(data: T, init?: RouterResponseInit): CustomResponse<T>;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from "./routers/index.js";
2
2
  export * from "./components.jsx";
3
3
  export * from "./lifecycle.js";
4
- export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing.js";
4
+ export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute } from "./routing.js";
5
5
  export { mergeSearchString as _mergeSearchString } from "./utils.js";
6
6
  export * from "./data/index.js";
7
- export type { Location, LocationChange, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RouteLoadFunc, RouteLoadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, BeforeLeaveEventArgs } from "./types.js";
7
+ export type { Location, LocationChange, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, BeforeLeaveEventArgs, RouteLoadFunc, RouteLoadFuncArgs, RouterResponseInit, CustomResponse } from "./types.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps as mergeProps$1, template } from 'solid-js/web';
2
- import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, createComponent, children, mergeProps, Show, createRoot, getListener, sharedConfig, $TRACK, splitProps, createResource } from 'solid-js';
2
+ import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, batch, createComponent, children, mergeProps, Show, createRoot, getListener, sharedConfig, $TRACK, splitProps, createResource } from 'solid-js';
3
3
  import { createStore, reconcile, unwrap } from 'solid-js/store';
4
4
 
5
5
  function createBeforeLeave() {
@@ -246,6 +246,7 @@ const useHref = to => {
246
246
  const useNavigate = () => useRouter().navigatorFactory();
247
247
  const useLocation = () => useRouter().location;
248
248
  const useIsRouting = () => useRouter().isRouting;
249
+ const usePreloadRoute = () => useRouter().preloadRoute;
249
250
  const useMatch = (path, matchFilters) => {
250
251
  const location = useLocation();
251
252
  const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters)));
@@ -282,6 +283,7 @@ const useBeforeLeave = listener => {
282
283
  function createRoutes(routeDef, base = "") {
283
284
  const {
284
285
  component,
286
+ preload,
285
287
  load,
286
288
  children,
287
289
  info
@@ -290,7 +292,7 @@ function createRoutes(routeDef, base = "") {
290
292
  const shared = {
291
293
  key: routeDef,
292
294
  component,
293
- load,
295
+ preload: preload || load,
294
296
  info
295
297
  };
296
298
  return asArray(routeDef.path).reduce((acc, originalPath) => {
@@ -407,12 +409,12 @@ let intent;
407
409
  function getIntent() {
408
410
  return intent;
409
411
  }
410
- let inLoadFn = false;
411
- function getInLoadFn() {
412
- return inLoadFn;
412
+ let inPreloadFn = false;
413
+ function getInPreloadFn() {
414
+ return inPreloadFn;
413
415
  }
414
- function setInLoadFn(value) {
415
- inLoadFn = value;
416
+ function setInPreloadFn(value) {
417
+ inPreloadFn = value;
416
418
  }
417
419
  function createRouterContext(integration, branches, getContext, options = {}) {
418
420
  const {
@@ -433,13 +435,33 @@ function createRouterContext(integration, branches, getContext, options = {}) {
433
435
  });
434
436
  }
435
437
  const [isRouting, setIsRouting] = createSignal(false);
436
- const start = async callback => {
437
- setIsRouting(true);
438
- try {
439
- await startTransition(callback);
440
- } finally {
441
- setIsRouting(false);
442
- }
438
+
439
+ // Keep track of last target, so that last call to transition wins
440
+ let lastTransitionTarget;
441
+
442
+ // Transition the location to a new value
443
+ const transition = (newIntent, newTarget) => {
444
+ if (newTarget.value === reference() && newTarget.state === state()) return;
445
+ if (lastTransitionTarget === undefined) setIsRouting(true);
446
+ intent = newIntent;
447
+ lastTransitionTarget = newTarget;
448
+ startTransition(() => {
449
+ if (lastTransitionTarget !== newTarget) return;
450
+ setReference(lastTransitionTarget.value);
451
+ setState(lastTransitionTarget.state);
452
+ resetErrorBoundaries();
453
+ if (!isServer) submissions[1]([]);
454
+ }).finally(() => {
455
+ if (lastTransitionTarget !== newTarget) return;
456
+
457
+ // Batch, in order for isRouting and final source update to happen together
458
+ batch(() => {
459
+ intent = undefined;
460
+ if (newIntent === "navigate") navigateEnd(lastTransitionTarget);
461
+ setIsRouting(false);
462
+ lastTransitionTarget = undefined;
463
+ });
464
+ });
443
465
  };
444
466
  const [reference, setReference] = createSignal(source().value);
445
467
  const [state, setState] = createSignal(source().state);
@@ -468,24 +490,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
468
490
  return resolvePath(basePath, to);
469
491
  }
470
492
  };
471
- createRenderEffect(() => {
472
- const {
473
- value,
474
- state
475
- } = source();
476
- // Untrack this whole block so `start` doesn't cause Solid's Listener to be preserved
477
- untrack(() => {
478
- start(() => {
479
- intent = "native";
480
- if (value !== reference()) setReference(value);
481
- setState(state);
482
- resetErrorBoundaries();
483
- submissions[1]([]);
484
- }).then(() => {
485
- intent = undefined;
486
- });
487
- });
488
- });
493
+
494
+ // Create a native transition, when source updates
495
+ createRenderEffect(on(source, source => transition("native", source), {
496
+ defer: true
497
+ }));
489
498
  return {
490
499
  base: baseRoute,
491
500
  location,
@@ -545,26 +554,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
545
554
  state: nextState
546
555
  });
547
556
  } else if (beforeLeave.confirm(resolvedTo, options)) {
548
- const len = referrers.push({
557
+ referrers.push({
549
558
  value: current,
550
559
  replace,
551
560
  scroll,
552
561
  state: state()
553
562
  });
554
- start(() => {
555
- intent = "navigate";
556
- setReference(resolvedTo);
557
- setState(nextState);
558
- resetErrorBoundaries();
559
- submissions[1]([]);
560
- }).then(() => {
561
- if (referrers.length === len) {
562
- intent = undefined;
563
- navigateEnd({
564
- value: resolvedTo,
565
- state: nextState
566
- });
567
- }
563
+ transition("navigate", {
564
+ value: resolvedTo,
565
+ state: nextState
568
566
  });
569
567
  }
570
568
  }
@@ -578,17 +576,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
578
576
  function navigateEnd(next) {
579
577
  const first = referrers[0];
580
578
  if (first) {
581
- if (next.value !== first.value || next.state !== first.state) {
582
- setSource({
583
- ...next,
584
- replace: first.replace,
585
- scroll: first.scroll
586
- });
587
- }
579
+ setSource({
580
+ ...next,
581
+ replace: first.replace,
582
+ scroll: first.scroll
583
+ });
588
584
  referrers.length = 0;
589
585
  }
590
586
  }
591
- function preloadRoute(url, preloadData) {
587
+ function preloadRoute(url, options = {}) {
592
588
  const matches = getRouteMatches(branches(), url.pathname);
593
589
  const prevIntent = intent;
594
590
  intent = "preload";
@@ -599,10 +595,10 @@ function createRouterContext(integration, branches, getContext, options = {}) {
599
595
  } = matches[match];
600
596
  route.component && route.component.preload && route.component.preload();
601
597
  const {
602
- load
598
+ preload
603
599
  } = route;
604
- inLoadFn = true;
605
- preloadData && load && runWithOwner(getContext(), () => load({
600
+ inPreloadFn = true;
601
+ options.preloadData && preload && runWithOwner(getContext(), () => preload({
606
602
  params,
607
603
  location: {
608
604
  pathname: url.pathname,
@@ -614,7 +610,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
614
610
  },
615
611
  intent: "preload"
616
612
  }));
617
- inLoadFn = false;
613
+ inPreloadFn = false;
618
614
  }
619
615
  intent = prevIntent;
620
616
  }
@@ -632,17 +628,17 @@ function createRouteContext(router, parent, outlet, match) {
632
628
  const {
633
629
  pattern,
634
630
  component,
635
- load
631
+ preload
636
632
  } = match().route;
637
633
  const path = createMemo(() => match().path);
638
634
  component && component.preload && component.preload();
639
- inLoadFn = true;
640
- const data = load ? load({
635
+ inPreloadFn = true;
636
+ const data = preload ? preload({
641
637
  params,
642
638
  location,
643
639
  intent: intent || "initial"
644
640
  }) : undefined;
645
- inLoadFn = false;
641
+ inPreloadFn = false;
646
642
  const route = {
647
643
  parent,
648
644
  pattern,
@@ -683,8 +679,8 @@ const createRouterComponent = router => props => {
683
679
  get root() {
684
680
  return props.root;
685
681
  },
686
- get load() {
687
- return props.rootLoad;
682
+ get preload() {
683
+ return props.rootPreload || props.rootLoad;
688
684
  },
689
685
  get children() {
690
686
  return [memo(() => (context = getOwner()) && null), createComponent$1(Routes, {
@@ -701,14 +697,14 @@ const createRouterComponent = router => props => {
701
697
  function Root(props) {
702
698
  const location = props.routerState.location;
703
699
  const params = props.routerState.params;
704
- const data = createMemo(() => props.load && untrack(() => {
705
- setInLoadFn(true);
706
- props.load({
700
+ const data = createMemo(() => props.preload && untrack(() => {
701
+ setInPreloadFn(true);
702
+ props.preload({
707
703
  params,
708
704
  location,
709
705
  intent: getIntent() || "initial"
710
706
  });
711
- setInLoadFn(false);
707
+ setInPreloadFn(false);
712
708
  }));
713
709
  return createComponent$1(Show, {
714
710
  get when() {
@@ -813,7 +809,7 @@ function dataOnly(event, routerState, branches) {
813
809
  route,
814
810
  params
815
811
  } = matches[match];
816
- route.load && route.load({
812
+ route.preload && route.preload({
817
813
  params,
818
814
  location: routerState.location,
819
815
  intent: "preload"
@@ -928,7 +924,7 @@ function cache(fn, name) {
928
924
  const cachedFn = (...args) => {
929
925
  const cache = getCache();
930
926
  const intent = getIntent();
931
- const inLoadFn = getInLoadFn();
927
+ const inPreloadFn = getInPreloadFn();
932
928
  const owner = getOwner();
933
929
  const navigate = owner ? useNavigate() : undefined;
934
930
  const now = Date.now();
@@ -962,10 +958,11 @@ function cache(fn, name) {
962
958
  cached[0] = now;
963
959
  }
964
960
  let res = cached[1];
965
- if (!inLoadFn) {
961
+ if (intent !== "preload") {
966
962
  res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
967
963
  !isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
968
- } else "then" in res && res.catch(() => {});
964
+ }
965
+ inPreloadFn && "then" in res && res.catch(() => {});
969
966
  return res;
970
967
  }
971
968
  let res = !isServer && sharedConfig.context && sharedConfig.has(key) ? sharedConfig.load(key) // hydrating
@@ -987,9 +984,10 @@ function cache(fn, name) {
987
984
  const e = getRequestEvent();
988
985
  if (e && e.router.dataOnly) return e.router.data[key] = res;
989
986
  }
990
- if (!inLoadFn) {
987
+ if (intent !== "preload") {
991
988
  res = "then" in res ? res.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(res);
992
- } else "then" in res && res.catch(() => {});
989
+ }
990
+ inPreloadFn && "then" in res && res.catch(() => {});
993
991
  // serialize on server
994
992
  if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) {
995
993
  const e = getRequestEvent();
@@ -999,18 +997,21 @@ function cache(fn, name) {
999
997
  function handleResponse(error) {
1000
998
  return async v => {
1001
999
  if (v instanceof Response) {
1002
- if (v.headers.has("Location")) {
1003
- if (navigate) {
1004
- startTransition(() => {
1005
- let url = v.headers.get(LocationHeader);
1006
- if (url && url.startsWith("/")) {
1007
- navigate(url, {
1008
- replace: true
1009
- });
1010
- } else if (!isServer && url) {
1011
- window.location.href = url;
1012
- }
1000
+ const url = v.headers.get(LocationHeader);
1001
+ if (url !== null) {
1002
+ // client + server relative redirect
1003
+ if (navigate && url.startsWith("/")) startTransition(() => {
1004
+ navigate(url, {
1005
+ replace: true
1013
1006
  });
1007
+ });else if (!isServer) window.location.href = url;else if (isServer) {
1008
+ const e = getRequestEvent();
1009
+ if (e) e.response = {
1010
+ status: 302,
1011
+ headers: new Headers({
1012
+ Location: url
1013
+ })
1014
+ };
1014
1015
  }
1015
1016
  return;
1016
1017
  }
@@ -1075,6 +1076,7 @@ function useSubmission(fn, filter) {
1075
1076
  const submissions = useSubmissions(fn, filter);
1076
1077
  return new Proxy({}, {
1077
1078
  get(_, property) {
1079
+ if (submissions.length === 0 && property === "clear" || property === "retry") return () => {};
1078
1080
  return submissions[submissions.length - 1]?.[property];
1079
1081
  }
1080
1082
  });
@@ -1232,7 +1234,9 @@ function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "
1232
1234
  if (typeof transformUrl === "function") {
1233
1235
  url.pathname = transformUrl(url.pathname);
1234
1236
  }
1235
- if (!preloadTimeout[url.pathname]) router.preloadRoute(url, a.getAttribute("preload") !== "false");
1237
+ if (!preloadTimeout[url.pathname]) router.preloadRoute(url, {
1238
+ preloadData: a.getAttribute("preload") !== "false"
1239
+ });
1236
1240
  }
1237
1241
  function handleAnchorIn(evt) {
1238
1242
  const res = handleAnchor(evt);
@@ -1243,7 +1247,9 @@ function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "
1243
1247
  }
1244
1248
  if (preloadTimeout[url.pathname]) return;
1245
1249
  preloadTimeout[url.pathname] = setTimeout(() => {
1246
- router.preloadRoute(url, a.getAttribute("preload") !== "false");
1250
+ router.preloadRoute(url, {
1251
+ preloadData: a.getAttribute("preload") !== "false"
1252
+ });
1247
1253
  delete preloadTimeout[url.pathname];
1248
1254
  }, 200);
1249
1255
  }
@@ -1273,12 +1279,11 @@ function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "
1273
1279
  const handler = actions.get(actionRef);
1274
1280
  if (handler) {
1275
1281
  evt.preventDefault();
1276
- const data = new FormData(evt.target);
1277
- if (evt.submitter && evt.submitter.name) data.append(evt.submitter.name, evt.submitter.value);
1282
+ const data = new FormData(evt.target, evt.submitter);
1278
1283
  handler.call({
1279
1284
  r: router,
1280
1285
  f: evt.target
1281
- }, data);
1286
+ }, evt.target.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
1282
1287
  }
1283
1288
  }
1284
1289
 
@@ -1308,7 +1313,7 @@ function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "
1308
1313
  function Router(props) {
1309
1314
  if (isServer) return StaticRouter(props);
1310
1315
  const getSource = () => {
1311
- const url = window.location.pathname + window.location.search;
1316
+ const url = window.location.pathname.replace(/^\/+/, "/") + window.location.search;
1312
1317
  return {
1313
1318
  value: props.transformUrl ? props.transformUrl(url) + window.location.hash : url + window.location.hash,
1314
1319
  state: window.history.state
@@ -1328,7 +1333,7 @@ function Router(props) {
1328
1333
  } else {
1329
1334
  window.history.pushState(state, "", value);
1330
1335
  }
1331
- scrollToHash(window.location.hash.slice(1), scroll);
1336
+ scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll);
1332
1337
  saveCurrentDepth();
1333
1338
  },
1334
1339
  init: notify => bindEvent(window, "popstate", notifyIfNotBlocked(notify, delta => {
@@ -1639,4 +1644,4 @@ function json(data, init = {}) {
1639
1644
  return response;
1640
1645
  }
1641
1646
 
1642
- export { A, HashRouter, MemoryRouter, Navigate, Route, Router, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
1647
+ export { A, HashRouter, MemoryRouter, Navigate, Route, Router, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
package/dist/index.jsx CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from "./routers/index.js";
2
2
  export * from "./components.jsx";
3
3
  export * from "./lifecycle.js";
4
- export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing.js";
4
+ export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute } from "./routing.js";
5
5
  export { mergeSearchString as _mergeSearchString } from "./utils.js";
6
6
  export * from "./data/index.js";
@@ -7,7 +7,7 @@ export function Router(props) {
7
7
  if (isServer)
8
8
  return StaticRouter(props);
9
9
  const getSource = () => {
10
- const url = window.location.pathname + window.location.search;
10
+ const url = window.location.pathname.replace(/^\/+/, "/") + window.location.search;
11
11
  return {
12
12
  value: props.transformUrl ? props.transformUrl(url) + window.location.hash : url + window.location.hash,
13
13
  state: window.history.state
@@ -23,7 +23,7 @@ export function Router(props) {
23
23
  else {
24
24
  window.history.pushState(state, "", value);
25
25
  }
26
- scrollToHash(window.location.hash.slice(1), scroll);
26
+ scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll);
27
27
  saveCurrentDepth();
28
28
  },
29
29
  init: notify => bindEvent(window, "popstate", notifyIfNotBlocked(notify, delta => {
@@ -1,23 +1,27 @@
1
1
  import type { Component, JSX } from "solid-js";
2
- import type { MatchFilters, RouteLoadFunc, RouteDefinition, RouterIntegration, RouteSectionProps } from "../types.js";
2
+ import type { MatchFilters, RouteDefinition, RouterIntegration, RouteSectionProps, RoutePreloadFunc } from "../types.js";
3
3
  export type BaseRouterProps = {
4
4
  base?: string;
5
5
  /**
6
6
  * A component that wraps the content of every route.
7
7
  */
8
8
  root?: Component<RouteSectionProps>;
9
- rootLoad?: RouteLoadFunc;
9
+ rootPreload?: RoutePreloadFunc;
10
10
  singleFlight?: boolean;
11
11
  children?: JSX.Element | RouteDefinition | RouteDefinition[];
12
12
  transformUrl?: (url: string) => string;
13
+ /** @deprecated use rootPreload */
14
+ rootLoad?: RoutePreloadFunc;
13
15
  };
14
16
  export declare const createRouterComponent: (router: RouterIntegration) => (props: BaseRouterProps) => JSX.Element;
15
17
  export type RouteProps<S extends string, T = unknown> = {
16
18
  path?: S | S[];
17
19
  children?: JSX.Element;
18
- load?: RouteLoadFunc<T>;
20
+ preload?: RoutePreloadFunc<T>;
19
21
  matchFilters?: MatchFilters<S>;
20
22
  component?: Component<RouteSectionProps<T>>;
21
23
  info?: Record<string, any>;
24
+ /** @deprecated use preload */
25
+ load?: RoutePreloadFunc<T>;
22
26
  };
23
27
  export declare const Route: <S extends string, T = unknown>(props: RouteProps<S, T>) => JSX.Element;
@@ -1,7 +1,7 @@
1
1
  /*@refresh skip*/
2
2
  import { getRequestEvent, isServer } from "solid-js/web";
3
3
  import { children, createMemo, createRoot, getOwner, mergeProps, on, Show, untrack } from "solid-js";
4
- import { createBranches, createRouteContext, createRouterContext, getIntent, getRouteMatches, RouteContextObj, RouterContextObj, setInLoadFn } from "../routing.js";
4
+ import { createBranches, createRouteContext, createRouterContext, getIntent, getRouteMatches, RouteContextObj, RouterContextObj, setInPreloadFn } from "../routing.js";
5
5
  export const createRouterComponent = (router) => (props) => {
6
6
  const { base } = props;
7
7
  const routeDefs = children(() => props.children);
@@ -14,7 +14,7 @@ export const createRouterComponent = (router) => (props) => {
14
14
  });
15
15
  router.create && router.create(routerState);
16
16
  return (<RouterContextObj.Provider value={routerState}>
17
- <Root routerState={routerState} root={props.root} load={props.rootLoad}>
17
+ <Root routerState={routerState} root={props.root} preload={props.rootPreload || props.rootLoad}>
18
18
  {(context = getOwner()) && null}
19
19
  <Routes routerState={routerState} branches={branches()}/>
20
20
  </Root>
@@ -23,11 +23,11 @@ export const createRouterComponent = (router) => (props) => {
23
23
  function Root(props) {
24
24
  const location = props.routerState.location;
25
25
  const params = props.routerState.params;
26
- const data = createMemo(() => props.load &&
26
+ const data = createMemo(() => props.preload &&
27
27
  untrack(() => {
28
- setInLoadFn(true);
29
- props.load({ params, location, intent: getIntent() || "initial" });
30
- setInLoadFn(false);
28
+ setInPreloadFn(true);
29
+ props.preload({ params, location, intent: getIntent() || "initial" });
30
+ setInPreloadFn(false);
31
31
  }));
32
32
  return (<Show when={props.root} keyed fallback={props.children}>
33
33
  {Root => (<Root params={params} location={location} data={data()}>
@@ -105,8 +105,8 @@ function dataOnly(event, routerState, branches) {
105
105
  if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
106
106
  event.router.dataOnly = true;
107
107
  const { route, params } = matches[match];
108
- route.load &&
109
- route.load({
108
+ route.preload &&
109
+ route.preload({
110
110
  params,
111
111
  location: routerState.location,
112
112
  intent: "preload"
package/dist/routing.d.ts CHANGED
@@ -9,6 +9,9 @@ export declare const useHref: (to: () => string | undefined) => Accessor<string
9
9
  export declare const useNavigate: () => Navigator;
10
10
  export declare const useLocation: <S = unknown>() => Location<S>;
11
11
  export declare const useIsRouting: () => () => boolean;
12
+ export declare const usePreloadRoute: () => (url: URL, options: {
13
+ preloadData?: boolean | undefined;
14
+ }) => void;
12
15
  export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S> | undefined) => Accessor<import("./types.js").PathMatch | undefined>;
13
16
  export declare const useCurrentMatches: () => () => RouteMatch[];
14
17
  export declare const useParams: <T extends Params>() => T;
@@ -20,8 +23,8 @@ export declare function createBranches(routeDef: RouteDefinition | RouteDefiniti
20
23
  export declare function getRouteMatches(branches: Branch[], location: string): RouteMatch[];
21
24
  export declare function createLocation(path: Accessor<string>, state: Accessor<any>): Location;
22
25
  export declare function getIntent(): Intent | undefined;
23
- export declare function getInLoadFn(): boolean;
24
- export declare function setInLoadFn(value: boolean): void;
26
+ export declare function getInPreloadFn(): boolean;
27
+ export declare function setInPreloadFn(value: boolean): void;
25
28
  export declare function createRouterContext(integration: RouterIntegration, branches: () => Branch[], getContext?: () => any, options?: {
26
29
  base?: string;
27
30
  singleFlight?: boolean;
package/dist/routing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { runWithOwner } from "solid-js";
1
+ import { runWithOwner, batch } from "solid-js";
2
2
  import { createComponent, createContext, createMemo, createRenderEffect, createSignal, on, onCleanup, untrack, useContext, startTransition, resetErrorBoundaries } from "solid-js";
3
3
  import { isServer, getRequestEvent } from "solid-js/web";
4
4
  import { createBeforeLeave } from "./lifecycle.js";
@@ -23,6 +23,7 @@ export const useHref = (to) => {
23
23
  export const useNavigate = () => useRouter().navigatorFactory();
24
24
  export const useLocation = () => useRouter().location;
25
25
  export const useIsRouting = () => useRouter().isRouting;
26
+ export const usePreloadRoute = () => useRouter().preloadRoute;
26
27
  export const useMatch = (path, matchFilters) => {
27
28
  const location = useLocation();
28
29
  const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters)));
@@ -58,12 +59,12 @@ export const useBeforeLeave = (listener) => {
58
59
  onCleanup(s);
59
60
  };
60
61
  export function createRoutes(routeDef, base = "") {
61
- const { component, load, children, info } = routeDef;
62
+ const { component, preload, load, children, info } = routeDef;
62
63
  const isLeaf = !children || (Array.isArray(children) && !children.length);
63
64
  const shared = {
64
65
  key: routeDef,
65
66
  component,
66
- load,
67
+ preload: preload || load,
67
68
  info
68
69
  };
69
70
  return asArray(routeDef.path).reduce((acc, originalPath) => {
@@ -185,12 +186,12 @@ let intent;
185
186
  export function getIntent() {
186
187
  return intent;
187
188
  }
188
- let inLoadFn = false;
189
- export function getInLoadFn() {
190
- return inLoadFn;
189
+ let inPreloadFn = false;
190
+ export function getInPreloadFn() {
191
+ return inPreloadFn;
191
192
  }
192
- export function setInLoadFn(value) {
193
- inLoadFn = value;
193
+ export function setInPreloadFn(value) {
194
+ inPreloadFn = value;
194
195
  }
195
196
  export function createRouterContext(integration, branches, getContext, options = {}) {
196
197
  const { signal: [source, setSource], utils = {} } = integration;
@@ -205,14 +206,36 @@ export function createRouterContext(integration, branches, getContext, options =
205
206
  setSource({ value: basePath, replace: true, scroll: false });
206
207
  }
207
208
  const [isRouting, setIsRouting] = createSignal(false);
208
- const start = async (callback) => {
209
- setIsRouting(true);
210
- try {
211
- await startTransition(callback);
212
- }
213
- finally {
214
- setIsRouting(false);
215
- }
209
+ // Keep track of last target, so that last call to transition wins
210
+ let lastTransitionTarget;
211
+ // Transition the location to a new value
212
+ const transition = (newIntent, newTarget) => {
213
+ if (newTarget.value === reference() && newTarget.state === state())
214
+ return;
215
+ if (lastTransitionTarget === undefined)
216
+ setIsRouting(true);
217
+ intent = newIntent;
218
+ lastTransitionTarget = newTarget;
219
+ startTransition(() => {
220
+ if (lastTransitionTarget !== newTarget)
221
+ return;
222
+ setReference(lastTransitionTarget.value);
223
+ setState(lastTransitionTarget.state);
224
+ resetErrorBoundaries();
225
+ if (!isServer)
226
+ submissions[1]([]);
227
+ }).finally(() => {
228
+ if (lastTransitionTarget !== newTarget)
229
+ return;
230
+ // Batch, in order for isRouting and final source update to happen together
231
+ batch(() => {
232
+ intent = undefined;
233
+ if (newIntent === "navigate")
234
+ navigateEnd(lastTransitionTarget);
235
+ setIsRouting(false);
236
+ lastTransitionTarget = undefined;
237
+ });
238
+ });
216
239
  };
217
240
  const [reference, setReference] = createSignal(source().value);
218
241
  const [state, setState] = createSignal(source().state);
@@ -241,22 +264,8 @@ export function createRouterContext(integration, branches, getContext, options =
241
264
  return resolvePath(basePath, to);
242
265
  }
243
266
  };
244
- createRenderEffect(() => {
245
- const { value, state } = source();
246
- // Untrack this whole block so `start` doesn't cause Solid's Listener to be preserved
247
- untrack(() => {
248
- start(() => {
249
- intent = "native";
250
- if (value !== reference())
251
- setReference(value);
252
- setState(state);
253
- resetErrorBoundaries();
254
- submissions[1]([]);
255
- }).then(() => {
256
- intent = undefined;
257
- });
258
- });
259
- });
267
+ // Create a native transition, when source updates
268
+ createRenderEffect(on(source, source => transition("native", source), { defer: true }));
260
269
  return {
261
270
  base: baseRoute,
262
271
  location,
@@ -307,21 +316,10 @@ export function createRouterContext(integration, branches, getContext, options =
307
316
  setSource({ value: resolvedTo, replace, scroll, state: nextState });
308
317
  }
309
318
  else if (beforeLeave.confirm(resolvedTo, options)) {
310
- const len = referrers.push({ value: current, replace, scroll, state: state() });
311
- start(() => {
312
- intent = "navigate";
313
- setReference(resolvedTo);
314
- setState(nextState);
315
- resetErrorBoundaries();
316
- submissions[1]([]);
317
- }).then(() => {
318
- if (referrers.length === len) {
319
- intent = undefined;
320
- navigateEnd({
321
- value: resolvedTo,
322
- state: nextState
323
- });
324
- }
319
+ referrers.push({ value: current, replace, scroll, state: state() });
320
+ transition("navigate", {
321
+ value: resolvedTo,
322
+ state: nextState
325
323
  });
326
324
  }
327
325
  }
@@ -335,17 +333,15 @@ export function createRouterContext(integration, branches, getContext, options =
335
333
  function navigateEnd(next) {
336
334
  const first = referrers[0];
337
335
  if (first) {
338
- if (next.value !== first.value || next.state !== first.state) {
339
- setSource({
340
- ...next,
341
- replace: first.replace,
342
- scroll: first.scroll
343
- });
344
- }
336
+ setSource({
337
+ ...next,
338
+ replace: first.replace,
339
+ scroll: first.scroll
340
+ });
345
341
  referrers.length = 0;
346
342
  }
347
343
  }
348
- function preloadRoute(url, preloadData) {
344
+ function preloadRoute(url, options = {}) {
349
345
  const matches = getRouteMatches(branches(), url.pathname);
350
346
  const prevIntent = intent;
351
347
  intent = "preload";
@@ -354,11 +350,11 @@ export function createRouterContext(integration, branches, getContext, options =
354
350
  route.component &&
355
351
  route.component.preload &&
356
352
  route.component.preload();
357
- const { load } = route;
358
- inLoadFn = true;
359
- preloadData &&
360
- load &&
361
- runWithOwner(getContext(), () => load({
353
+ const { preload } = route;
354
+ inPreloadFn = true;
355
+ options.preloadData &&
356
+ preload &&
357
+ runWithOwner(getContext(), () => preload({
362
358
  params,
363
359
  location: {
364
360
  pathname: url.pathname,
@@ -370,27 +366,25 @@ export function createRouterContext(integration, branches, getContext, options =
370
366
  },
371
367
  intent: "preload"
372
368
  }));
373
- inLoadFn = false;
369
+ inPreloadFn = false;
374
370
  }
375
371
  intent = prevIntent;
376
372
  }
377
373
  function initFromFlash() {
378
374
  const e = getRequestEvent();
379
- return (e && e.router && e.router.submission
380
- ? [e.router.submission]
381
- : []);
375
+ return (e && e.router && e.router.submission ? [e.router.submission] : []);
382
376
  }
383
377
  }
384
378
  export function createRouteContext(router, parent, outlet, match) {
385
379
  const { base, location, params } = router;
386
- const { pattern, component, load } = match().route;
380
+ const { pattern, component, preload } = match().route;
387
381
  const path = createMemo(() => match().path);
388
382
  component &&
389
383
  component.preload &&
390
384
  component.preload();
391
- inLoadFn = true;
392
- const data = load ? load({ params, location, intent: intent || "initial" }) : undefined;
393
- inLoadFn = false;
385
+ inPreloadFn = true;
386
+ const data = preload ? preload({ params, location, intent: intent || "initial" }) : undefined;
387
+ inPreloadFn = false;
394
388
  const route = {
395
389
  parent,
396
390
  pattern,
package/dist/types.d.ts CHANGED
@@ -56,12 +56,12 @@ export interface RouterIntegration {
56
56
  utils?: Partial<RouterUtils>;
57
57
  }
58
58
  export type Intent = "initial" | "native" | "navigate" | "preload";
59
- export interface RouteLoadFuncArgs {
59
+ export interface RoutePreloadFuncArgs {
60
60
  params: Params;
61
61
  location: Location;
62
62
  intent: Intent;
63
63
  }
64
- export type RouteLoadFunc<T = unknown> = (args: RouteLoadFuncArgs) => T;
64
+ export type RoutePreloadFunc<T = unknown> = (args: RoutePreloadFuncArgs) => T;
65
65
  export interface RouteSectionProps<T = unknown> {
66
66
  params: Params;
67
67
  location: Location;
@@ -71,10 +71,12 @@ export interface RouteSectionProps<T = unknown> {
71
71
  export type RouteDefinition<S extends string | string[] = any, T = unknown> = {
72
72
  path?: S;
73
73
  matchFilters?: MatchFilters<S>;
74
- load?: RouteLoadFunc<T>;
74
+ preload?: RoutePreloadFunc<T>;
75
75
  children?: RouteDefinition | RouteDefinition[];
76
76
  component?: Component<RouteSectionProps<T>>;
77
77
  info?: Record<string, any>;
78
+ /** @deprecated use preload */
79
+ load?: RoutePreloadFunc;
78
80
  };
79
81
  export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean);
80
82
  export type PathParams<P extends string | readonly string[]> = P extends `${infer Head}/${infer Tail}` ? [...PathParams<Head>, ...PathParams<Tail>] : P extends `:${infer S}?` ? [S] : P extends `:${infer S}` ? [S] : P extends `*${infer S}` ? [S] : [];
@@ -100,7 +102,7 @@ export interface RouteDescription {
100
102
  originalPath: string;
101
103
  pattern: string;
102
104
  component?: Component<RouteSectionProps>;
103
- load?: RouteLoadFunc;
105
+ preload?: RoutePreloadFunc;
104
106
  matcher: (location: string) => PathMatch | null;
105
107
  matchFilters?: MatchFilters;
106
108
  info?: Record<string, any>;
@@ -134,7 +136,9 @@ export interface RouterContext {
134
136
  renderPath(path: string): string;
135
137
  parsePath(str: string): string;
136
138
  beforeLeave: BeforeLeaveLifecycle;
137
- preloadRoute: (url: URL, preloadData: boolean) => void;
139
+ preloadRoute: (url: URL, options: {
140
+ preloadData?: boolean;
141
+ }) => void;
138
142
  singleFlight: boolean;
139
143
  submissions: Signal<Submission<any, any>[]>;
140
144
  }
@@ -164,9 +168,30 @@ export type Submission<T, U> = {
164
168
  clear: () => void;
165
169
  retry: () => void;
166
170
  };
171
+ export type SubmissionStub = {
172
+ readonly input: undefined;
173
+ readonly result: undefined;
174
+ readonly error: undefined;
175
+ readonly pending: undefined;
176
+ readonly url: undefined;
177
+ clear: () => void;
178
+ retry: () => void;
179
+ };
167
180
  export interface MaybePreloadableComponent extends Component {
168
181
  preload?: () => void;
169
182
  }
170
183
  export type CacheEntry = [number, any, Intent | undefined, Signal<number> & {
171
184
  count: number;
172
185
  }];
186
+ export type NarrowResponse<T> = T extends CustomResponse<infer U> ? U : Exclude<T, Response>;
187
+ export type RouterResponseInit = Omit<ResponseInit, "body"> & {
188
+ revalidate?: string | string[];
189
+ };
190
+ export type CustomResponse<T> = Omit<Response, "clone"> & {
191
+ customBody: () => T;
192
+ clone(...args: readonly unknown[]): CustomResponse<T>;
193
+ };
194
+ /** @deprecated */
195
+ export type RouteLoadFunc = RoutePreloadFunc;
196
+ /** @deprecated */
197
+ export type RouteLoadFuncArgs = RoutePreloadFuncArgs;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.13.5",
9
+ "version": "0.14.0",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",