You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@solidjs/router

Package Overview
Dependencies
Maintainers
2
Versions
74
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@solidjs/router - npm Package Compare versions

Comparing version
0.16.1
to
0.17.0-next.0
+22
-16
dist/components.jsx

@@ -1,14 +0,22 @@

import { createMemo, mergeProps, splitProps } from "solid-js";
import { createMemo, merge, omit } from "solid-js";
import { useHref, useLocation, useNavigate, useResolvedPath } from "./routing.js";
import { normalizePath } from "./utils.js";
function toClassName(value) {
if (!value)
return "";
if (typeof value === "string" || typeof value === "number")
return String(value);
if (Array.isArray(value))
return value.map(toClassName).filter(Boolean).join(" ");
if (typeof value === "object") {
return Object.entries(value)
.filter(([, enabled]) => enabled)
.map(([name]) => name)
.join(" ");
}
return "";
}
export function A(props) {
props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props);
const [, rest] = splitProps(props, [
"href",
"state",
"class",
"activeClass",
"inactiveClass",
"end"
]);
props = merge({ inactiveClass: "inactive", activeClass: "active" }, props);
const rest = omit(props, "href", "state", "class", "activeClass", "inactiveClass", "end");
const to = useResolvedPath(() => props.href);

@@ -25,8 +33,6 @@ const href = useHref(to);

});
return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{
...(props.class && { [props.class]: true }),
[props.inactiveClass]: !isActive()[0],
[props.activeClass]: isActive()[0],
...rest.classList
}} link aria-current={isActive()[1] ? "page" : undefined}/>);
const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass]
.filter(Boolean)
.join(" "));
return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} class={className()} link aria-current={isActive()[1] ? "page" : undefined}/>);
}

@@ -33,0 +39,0 @@ export function Navigate(props) {

import { JSX } from "solid-js";
import type { Submission, SubmissionStub, NarrowResponse } from "../types.js";
import type { Submission, NarrowResponse } from "../types.js";
export type Action<T extends Array<any>, U, V = T> = (T extends [FormData | URLSearchParams] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
url: string;
with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<NarrowResponse<U>>, ...args: A): Action<B, U, V>;
onSubmit(hook: (...args: V extends Array<any> ? V : T) => void): Action<T, U, V>;
onSettled(hook: (submission: Submission<V extends Array<any> ? V : T, NarrowResponse<U>>) => void): Action<T, U, V>;
};
type ActionFactory = {
<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>;
<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, options?: {
name?: string;
}): Action<T, U>;
};
export declare const actions: Map<string, Action<any, any, any>>;
export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>>[] & {
pending: boolean;
};
export declare function useSubmission<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>> | SubmissionStub;
export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<V, NarrowResponse<U>>[];
export declare function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>): (...args: Parameters<Action<T, U, V>>) => Promise<NarrowResponse<U>>;
export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>;
export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, options?: {
name?: string;
onComplete?: (s: Submission<T, U>) => void;
}): Action<T, U>;
export declare const action: ActionFactory;
export {};

@@ -1,6 +0,9 @@

import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js";
import { isServer } from "solid-js/web";
import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
import { isServer } from "@solidjs/web";
import { useRouter } from "../routing.js";
import { mockBase, setFunctionName } from "../utils.js";
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
const settledHooksSymbol = Symbol("routerActionSettledHooks");
const invokeSymbol = Symbol("routerActionInvoke");
export const actions = /* #__PURE__ */ new Map();

@@ -14,4 +17,2 @@ export function useSubmissions(fn, filter) {

return subs();
if (property === "pending")
return subs().some(sub => !sub.result);
return subs()[property];

@@ -24,12 +25,2 @@ },

}
export function useSubmission(fn, filter) {
const submissions = useSubmissions(fn, filter);
return new Proxy({}, {
get(_, property) {
if ((submissions.length === 0 && property === "clear") || property === "retry")
return () => { };
return submissions[submissions.length - 1]?.[property];
}
});
}
export function useAction(action) {

@@ -39,59 +30,55 @@ const r = useRouter();

}
export function action(fn, options = {}) {
function mutate(...variables) {
function actionImpl(fn, options = {}) {
async function invoke(variables, current) {
const router = this.r;
const form = this.f;
const p = (router.singleFlight && fn.withOptions
const submitHooks = current[submitHooksSymbol];
const settledHooks = current[settledHooksSymbol];
const runMutation = () => (router.singleFlight && fn.withOptions
? fn.withOptions({ headers: { "X-Single-Flight": "true" } })
: fn)(...variables);
const [result, setResult] = createSignal();
const run = createSolidAction(async function* (context) {
context.optimistic?.();
try {
const value = await context.call();
yield;
return { error: false, value };
}
catch (error) {
yield;
return { error: true, value: error };
}
});
const settled = await settleActionResult(run({
call: runMutation,
optimistic: submitHooks.size
? () => {
for (const hook of submitHooks.values())
hook(...variables);
}
: undefined
}));
const response = await handleResponse(settled.value, settled.error, router.navigatorFactory());
if (!response)
return undefined;
let submission;
function handler(error) {
return async (res) => {
const result = await handleResponse(res, error, router.navigatorFactory());
let retry = null;
o.onComplete?.({
...submission,
result: result?.data,
error: result?.error,
pending: false,
retry() {
return (retry = submission.retry());
}
});
if (retry)
return retry;
if (!result)
return submission.clear();
setResult(result);
if (result.error && !form)
throw result.error;
return result.data;
};
}
router.submissions[1](s => [
...s,
(submission = {
input: variables,
url,
get result() {
return result()?.data;
},
get error() {
return result()?.error;
},
get pending() {
return !result();
},
clear() {
router.submissions[1](v => v.filter(i => i !== submission));
},
retry() {
setResult(undefined);
const p = fn(...variables);
return p.then(handler(), handler(true));
}
})
]);
return p.then(handler(), handler(true));
submission = {
input: variables,
url,
result: response.data,
error: response.error,
clear() {
router.submissions[1](entries => entries.filter(entry => entry !== submission));
},
retry() {
submission.clear();
return current[invokeSymbol].call({ r: router, f: form }, variables, current);
}
};
router.submissions[1](entries => [...entries, submission]);
for (const hook of settledHooks.values())
hook(submission);
if (response.error && !form)
throw response.error;
return response.data;
}

@@ -101,8 +88,12 @@ const o = typeof options === "string" ? { name: options } : options;

const url = fn.url || (name && `https://action/${name}`) || "";
mutate.base = url;
const wrapped = toAction(invoke, url);
if (name)
setFunctionName(mutate, name);
return toAction(mutate, url);
setFunctionName(wrapped, name);
return wrapped;
}
function toAction(fn, url) {
export const action = actionImpl;
function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map(), settledHooks = new Map()) {
const fn = function (...args) {
return invoke.call(this, [...boundArgs, ...args], fn);
};
fn.toString = () => {

@@ -114,11 +105,24 @@ if (!url)

fn.with = function (...args) {
const newFn = function (...passedArgs) {
return fn.call(this, ...args, ...passedArgs);
};
newFn.base = fn.base;
const uri = new URL(url, mockBase);
uri.searchParams.set("args", hashKey(args));
return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
return next;
};
fn.onSubmit = function (hook) {
const id = Symbol("actionOnSubmitHook");
submitHooks.set(id, hook);
getOwner() && onCleanup(() => submitHooks.delete(id));
return this;
};
fn.onSettled = function (hook) {
const id = Symbol("actionOnSettledHook");
settledHooks.set(id, hook);
getOwner() && onCleanup(() => settledHooks.delete(id));
return this;
};
fn.url = url;
fn.base = base;
fn[submitHooksSymbol] = submitHooks;
fn[settledHooksSymbol] = settledHooks;
fn[invokeSymbol] = invoke;
if (!isServer) {

@@ -131,2 +135,17 @@ actions.set(url, fn);

const hashString = (s) => s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
async function settleActionResult(result) {
const value = result;
if (value && typeof value.then === "function") {
return result.then(value => value);
}
if (value && typeof value.next === "function") {
const iterator = value;
let next = await iterator.next();
while (!next.done) {
next = await iterator.next();
}
return next.value;
}
return result;
}
async function handleResponse(response, error, navigate) {

@@ -133,0 +152,0 @@ let data;

@@ -1,2 +0,2 @@

import { delegateEvents } from "solid-js/web";
import { delegateEvents } from "@solidjs/web";
import { onCleanup } from "solid-js";

@@ -3,0 +3,0 @@ import { actions } from "./action.js";

@@ -1,4 +0,3 @@

export { createAsync, createAsyncStore, type AccessorWithLatest } from "./createAsync.js";
export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js";
export { query, revalidate, cache, type CachedFunction } from "./query.js";
export { action, useSubmissions, useAction, type Action } from "./action.js";
export { query, revalidate, type CachedFunction } from "./query.js";
export { redirect, reload, json } from "./response.js";

@@ -1,4 +0,3 @@

export { createAsync, createAsyncStore } from "./createAsync.js";
export { action, useSubmission, useSubmissions, useAction } from "./action.js";
export { query, revalidate, cache } from "./query.js";
export { action, useSubmissions, useAction } from "./action.js";
export { query, revalidate } from "./query.js";
export { redirect, reload, json } from "./response.js";

@@ -5,3 +5,3 @@ import type { CacheEntry, NarrowResponse } from "../types.js";

*/
export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
export declare function revalidate(key?: string | string[] | void, force?: boolean): void;
export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;

@@ -22,4 +22,2 @@ export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends {

}
/** @deprecated use query instead */
export declare const cache: typeof query;
export declare function hashKey<T extends Array<any>>(args: T): string;

@@ -1,3 +0,3 @@

import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
import { getRequestEvent, isServer } from "solid-js/web";
import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js";
import { getRequestEvent, isServer } from "@solidjs/web";
import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";

@@ -31,8 +31,6 @@ const LocationHeader = "Location";

export function revalidate(key, force = true) {
return startTransition(() => {
const now = Date.now();
cacheKeyOp(key, entry => {
force && (entry[0] = 0); //force cache miss
entry[4][1](now); // retrigger live signals
});
const now = Date.now();
cacheKeyOp(key, entry => {
force && (entry[0] = 0); //force cache miss
entry[4][1](now); // retrigger live signals
});

@@ -76,3 +74,3 @@ }

}
if (getListener() && !isServer) {
if (getObserver() && !isServer) {
tracking = true;

@@ -100,3 +98,3 @@ onCleanup(() => cached[4].count--);

: handleResponse(false)(cached[1]);
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
}

@@ -118,3 +116,3 @@ inPreloadFn && "then" in res && res.catch(() => { });

cached[3] = intent;
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
}

@@ -166,5 +164,3 @@ else {

if (navigate && url.startsWith("/"))
startTransition(() => {
navigate(url, { replace: true });
});
navigate(url, { replace: true });
else if (!isServer)

@@ -211,4 +207,2 @@ window.location.href = url;

query.clear = () => getCache().clear();
/** @deprecated use query instead */
export const cache = query;
function matchKey(key, keys) {

@@ -215,0 +209,0 @@ for (let k of keys) {

@@ -7,2 +7,2 @@ export * from "./routers/index.js";

export * from "./data/index.js";
export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs, RouteLoadFunc, RouteLoadFuncArgs, RouterResponseInit, CustomResponse } from "./types.js";
export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs, RouterResponseInit, CustomResponse } from "./types.js";

@@ -1,4 +0,3 @@

import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps as mergeProps$1, template } from 'solid-js/web';
import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, batch, createComponent, children, mergeProps, Show, createRoot, sharedConfig, getListener, $TRACK, splitProps, createResource, catchError } from 'solid-js';
import { createStore, reconcile, unwrap } from 'solid-js/store';
import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps, template } from '@solidjs/web';
import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, flush, createComponent, children, merge, createRoot, sharedConfig, getObserver, $TRACK, action as action$1, omit } from 'solid-js';

@@ -254,4 +253,11 @@ function createBeforeLeave() {

const RouteContextObj = createContext();
function useOptionalContext(context) {
try {
return useContext(context);
} catch {
return undefined;
}
}
const useRouter = () => invariant(useContext(RouterContextObj), "<A> and 'use' router primitives can be only used inside a Route.");
const useRoute = () => useContext(RouteContextObj) || useRouter().base;
const useRoute = () => useOptionalContext(RouteContextObj) || useRouter().base;
const useResolvedPath = path => {

@@ -303,4 +309,4 @@ const route = useRoute();

/**
* Retrieves signal that indicates whether the route is currently in a *Transition*.
* Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering.
* Retrieves a signal that indicates whether the router is currently processing a navigation.
* Useful for showing pending navigation state while the next route and its data settle.
*

@@ -464,3 +470,2 @@ * @example

preload,
load,
children,

@@ -473,3 +478,3 @@ info

component,
preload: preload || load,
preload,
info

@@ -568,3 +573,3 @@ };

const key = () => "";
const queryFn = on(search, () => extractSearchParams(url()));
const queryFn = createMemo(() => extractSearchParams(url()));
return {

@@ -618,36 +623,31 @@ get pathname() {

}
const [isRouting, setIsRouting] = createSignal(false);
const [isRouting, setIsRouting] = createSignal(false, {
pureWrite: true
});
// Keep track of last target, so that last call to transition wins
// Navigate override written from event handlers.
const [navigateTarget, setNavigateTarget] = createSignal(undefined, {
pureWrite: true
});
// Keep track of last target, so that last call to navigate wins
let lastTransitionTarget;
// Transition the location to a new value
const transition = (newIntent, newTarget) => {
if (newTarget.value === reference() && newTarget.state === state()) return;
if (lastTransitionTarget === undefined) setIsRouting(true);
intent = newIntent;
lastTransitionTarget = newTarget;
startTransition(() => {
if (lastTransitionTarget !== newTarget) return;
setReference(lastTransitionTarget.value);
setState(lastTransitionTarget.state);
resetErrorBoundaries();
if (!isServer) submissions[1](subs => subs.filter(s => s.pending));
}).finally(() => {
if (lastTransitionTarget !== newTarget) return;
// Batch, in order for isRouting and final source update to happen together
batch(() => {
intent = undefined;
if (newIntent === "navigate") navigateEnd(lastTransitionTarget);
setIsRouting(false);
lastTransitionTarget = undefined;
});
});
};
const [reference, setReference] = createSignal(source().value);
const [state, setState] = createSignal(source().state);
// source() remains canonical for native history changes; navigateTarget()
// temporarily overrides it for in-flight programmatic navigation.
const reference = createMemo(() => {
const nav = navigateTarget();
if (nav !== undefined) return nav.value;
return source().value;
});
const state = createMemo(() => {
const nav = navigateTarget();
if (nav !== undefined) return nav.state;
return source().state;
});
const location = createLocation(reference, state, utils.queryWrapper);
const referrers = [];
const submissions = createSignal(isServer ? initFromFlash() : []);
const submissions = createSignal(isServer ? initFromFlash() : [], {
pureWrite: true
});
const matches = createMemo(() => {

@@ -676,7 +676,2 @@ if (typeof options.transformUrl === "function") {

};
// Create a native transition, when source updates
createRenderEffect(on(source, source => transition("native", source), {
defer: true
}));
return {

@@ -748,6 +743,25 @@ base: baseRoute,

});
transition("navigate", {
const newTarget = {
value: resolvedTo,
state: nextState
});
};
if (lastTransitionTarget === undefined) {
setIsRouting(true);
flush();
}
intent = "navigate";
lastTransitionTarget = newTarget;
if (lastTransitionTarget === newTarget) {
setNavigateTarget({
...lastTransitionTarget
});
queueMicrotask(() => {
if (lastTransitionTarget !== newTarget) return;
intent = undefined;
navigateEnd(lastTransitionTarget);
setNavigateTarget(undefined);
setIsRouting(false);
lastTransitionTarget = undefined;
});
}
}

@@ -759,3 +773,3 @@ }

// Workaround for vite issue (https://github.com/vitejs/vite/issues/3803)
route = route || useContext(RouteContextObj) || baseRoute;
route = route || useOptionalContext(RouteContextObj) || baseRoute;
return (to, options) => navigateFromRoute(route, to, options);

@@ -806,3 +820,8 @@ }

const e = getRequestEvent();
return e && e.router && e.router.submission ? [e.router.submission] : [];
if (!(e && e.router && e.router.submission)) return [];
return [{
...e.router.submission,
clear() {},
retry() {}
}];
}

@@ -862,3 +881,3 @@ }

router.create && router.create(routerState);
return createComponent$1(RouterContextObj.Provider, {
return createComponent$1(RouterContextObj, {
value: routerState,

@@ -872,3 +891,3 @@ get children() {

get preload() {
return props.rootPreload || props.rootLoad;
return props.rootPreload;
},

@@ -899,11 +918,5 @@ get children() {

}));
return createComponent$1(Show, {
get when() {
return props.root;
},
keyed: true,
get fallback() {
return props.children;
},
children: Root => createComponent$1(Root, {
const RootComp = props.root;
if (RootComp) {
return createComponent$1(RootComp, {
params: params,

@@ -917,4 +930,5 @@ location: location,

}
})
});
});
}
return props.children;
}

@@ -942,7 +956,10 @@ function Routes(props) {

let root;
const routeStates = createMemo(on(props.routerState.matches, (nextMatches, prevMatches, prev) => {
let equal = prevMatches && nextMatches.length === prevMatches.length;
let prevMatches;
const routeStates = createMemo(prev => {
const nextMatches = props.routerState.matches();
const previousMatches = prevMatches;
let equal = previousMatches && nextMatches.length === previousMatches.length;
const next = [];
for (let i = 0, len = nextMatches.length; i < len; i++) {
const prevMatch = prevMatches && prevMatches[i];
const prevMatch = previousMatches && previousMatches[i];
const nextMatch = nextMatches[i];

@@ -958,3 +975,3 @@ if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) {

disposers[i] = dispose;
next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()[i + 1]), () => {
next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => {
const routeMatches = props.routerState.matches();

@@ -968,26 +985,28 @@ return routeMatches[i] ?? routeMatches[0];

if (prev && equal) {
prevMatches = nextMatches;
return prev;
}
root = next[0];
prevMatches = nextMatches;
return next;
}));
}, undefined);
return createOutlet(() => routeStates() && root)();
}
const createOutlet = child => {
return () => createComponent$1(Show, {
get when() {
return child();
},
keyed: true,
children: child => createComponent$1(RouteContextObj.Provider, {
value: child,
get children() {
return child.outlet();
}
})
});
return () => {
const c = child();
if (c) {
return createComponent$1(RouteContextObj, {
value: c,
get children() {
return c.outlet();
}
});
}
return undefined;
};
};
const Route = props => {
const childRoutes = children(() => props.children);
return mergeProps(props, {
return merge(props, {
get children() {

@@ -1098,8 +1117,6 @@ return childRoutes();

function revalidate(key, force = true) {
return startTransition(() => {
const now = Date.now();
cacheKeyOp(key, entry => {
force && (entry[0] = 0); //force cache miss
entry[4][1](now); // retrigger live signals
});
const now = Date.now();
cacheKeyOp(key, entry => {
force && (entry[0] = 0); //force cache miss
entry[4][1](now); // retrigger live signals
});

@@ -1140,3 +1157,3 @@ }

}
if (getListener() && !isServer) {
if (getObserver() && !isServer) {
tracking = true;

@@ -1156,3 +1173,3 @@ onCleanup(() => cached[4].count--);

res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
}

@@ -1172,3 +1189,3 @@ inPreloadFn && "then" in res && res.catch(() => {});

cached[3] = intent;
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
} else {

@@ -1208,6 +1225,4 @@ cache.set(key, cached = [now, res,, intent, createSignal(now)]);

// client + server relative redirect
if (navigate && url.startsWith("/")) startTransition(() => {
navigate(url, {
replace: true
});
if (navigate && url.startsWith("/")) navigate(url, {
replace: true
});else if (!isServer) window.location.href = url;else if (e) e.response.status = 302;

@@ -1248,5 +1263,2 @@ return;

query.clear = () => getCache().clear();
/** @deprecated use query instead */
const cache = query;
function matchKey(key, keys) {

@@ -1272,2 +1284,5 @@ for (let k of keys) {

const submitHooksSymbol = Symbol("routerActionSubmitHooks");
const settledHooksSymbol = Symbol("routerActionSettledHooks");
const invokeSymbol = Symbol("routerActionInvoke");
const actions = /* #__PURE__ */new Map();

@@ -1280,3 +1295,2 @@ function useSubmissions(fn, filter) {

if (property === $TRACK) return subs();
if (property === "pending") return subs().some(sub => !sub.result);
return subs()[property];

@@ -1289,11 +1303,2 @@ },

}
function useSubmission(fn, filter) {
const submissions = useSubmissions(fn, filter);
return new Proxy({}, {
get(_, property) {
if (submissions.length === 0 && property === "clear" || property === "retry") return () => {};
return submissions[submissions.length - 1]?.[property];
}
});
}
function useAction(action) {

@@ -1305,7 +1310,9 @@ const r = useRouter();

}
function action(fn, options = {}) {
function mutate(...variables) {
function actionImpl(fn, options = {}) {
async function invoke(variables, current) {
const router = this.r;
const form = this.f;
const p = (router.singleFlight && fn.withOptions ? fn.withOptions({
const submitHooks = current[submitHooksSymbol];
const settledHooks = current[settledHooksSymbol];
const runMutation = () => (router.singleFlight && fn.withOptions ? fn.withOptions({
headers: {

@@ -1315,46 +1322,48 @@ "X-Single-Flight": "true"

}) : fn)(...variables);
const [result, setResult] = createSignal();
const run = action$1(async function* (context) {
context.optimistic?.();
try {
const value = await context.call();
yield;
return {
error: false,
value
};
} catch (error) {
yield;
return {
error: true,
value: error
};
}
});
const settled = await settleActionResult(run({
call: runMutation,
optimistic: submitHooks.size ? () => {
for (const hook of submitHooks.values()) hook(...variables);
} : undefined
}));
const response = await handleResponse(settled.value, settled.error, router.navigatorFactory());
if (!response) return undefined;
let submission;
function handler(error) {
return async res => {
const result = await handleResponse(res, error, router.navigatorFactory());
let retry = null;
o.onComplete?.({
...submission,
result: result?.data,
error: result?.error,
pending: false,
retry() {
return retry = submission.retry();
}
});
if (retry) return retry;
if (!result) return submission.clear();
setResult(result);
if (result.error && !form) throw result.error;
return result.data;
};
}
router.submissions[1](s => [...s, submission = {
submission = {
input: variables,
url,
get result() {
return result()?.data;
},
get error() {
return result()?.error;
},
get pending() {
return !result();
},
result: response.data,
error: response.error,
clear() {
router.submissions[1](v => v.filter(i => i !== submission));
router.submissions[1](entries => entries.filter(entry => entry !== submission));
},
retry() {
setResult(undefined);
const p = fn(...variables);
return p.then(handler(), handler(true));
submission.clear();
return current[invokeSymbol].call({
r: router,
f: form
}, variables, current);
}
}]);
return p.then(handler(), handler(true));
};
router.submissions[1](entries => [...entries, submission]);
for (const hook of settledHooks.values()) hook(submission);
if (response.error && !form) throw response.error;
return response.data;
}

@@ -1366,7 +1375,11 @@ const o = typeof options === "string" ? {

const url = fn.url || name && `https://action/${name}` || "";
mutate.base = url;
if (name) setFunctionName(mutate, name);
return toAction(mutate, url);
const wrapped = toAction(invoke, url);
if (name) setFunctionName(wrapped, name);
return wrapped;
}
function toAction(fn, url) {
const action = actionImpl;
function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map(), settledHooks = new Map()) {
const fn = function (...args) {
return invoke.call(this, [...boundArgs, ...args], fn);
};
fn.toString = () => {

@@ -1377,11 +1390,24 @@ if (!url) throw new Error("Client Actions need explicit names if server rendered");

fn.with = function (...args) {
const newFn = function (...passedArgs) {
return fn.call(this, ...args, ...passedArgs);
};
newFn.base = fn.base;
const uri = new URL(url, mockBase);
uri.searchParams.set("args", hashKey(args));
return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
return next;
};
fn.onSubmit = function (hook) {
const id = Symbol("actionOnSubmitHook");
submitHooks.set(id, hook);
getOwner() && onCleanup(() => submitHooks.delete(id));
return this;
};
fn.onSettled = function (hook) {
const id = Symbol("actionOnSettledHook");
settledHooks.set(id, hook);
getOwner() && onCleanup(() => settledHooks.delete(id));
return this;
};
fn.url = url;
fn.base = base;
fn[submitHooksSymbol] = submitHooks;
fn[settledHooksSymbol] = settledHooks;
fn[invokeSymbol] = invoke;
if (!isServer) {

@@ -1394,2 +1420,17 @@ actions.set(url, fn);

const hashString = s => s.split("").reduce((a, b) => (a << 5) - a + b.charCodeAt(0) | 0, 0);
async function settleActionResult(result) {
const value = result;
if (value && typeof value.then === "function") {
return result.then(value => value);
}
if (value && typeof value.next === "function") {
const iterator = value;
let next = await iterator.next();
while (!next.done) {
next = await iterator.next();
}
return next.value;
}
return result;
}
async function handleResponse(response, error, navigate) {

@@ -1703,8 +1744,17 @@ let data;

var _tmpl$ = /*#__PURE__*/template(`<a>`);
function toClassName(value) {
if (!value) return "";
if (typeof value === "string" || typeof value === "number") return String(value);
if (Array.isArray(value)) return value.map(toClassName).filter(Boolean).join(" ");
if (typeof value === "object") {
return Object.entries(value).filter(([, enabled]) => enabled).map(([name]) => name).join(" ");
}
return "";
}
function A(props) {
props = mergeProps({
props = merge({
inactiveClass: "inactive",
activeClass: "active"
}, props);
const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]);
const rest = omit(props, "href", "state", "class", "activeClass", "inactiveClass", "end");
const to = useResolvedPath(() => props.href);

@@ -1720,5 +1770,6 @@ const href = useHref(to);

});
const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass].filter(Boolean).join(" "));
return (() => {
var _el$ = _tmpl$();
spread(_el$, mergeProps$1(rest, {
spread(_el$, mergeProps(rest, {
get href() {

@@ -1730,13 +1781,6 @@ return href() || props.href;

},
get classList() {
return {
...(props.class && {
[props.class]: true
}),
[props.inactiveClass]: !isActive()[0],
[props.activeClass]: isActive()[0],
...rest.classList
};
get ["class"]() {
return className();
},
"link": "",
"link": true,
get ["aria-current"]() {

@@ -1767,95 +1811,2 @@ return isActive()[1] ? "page" : undefined;

/**
* This is mock of the eventual Solid 2.0 primitive. It is not fully featured.
*/
/**
* As `createAsync` and `createAsyncStore` are wrappers for `createResource`,
* this type allows to support `latest` field for these primitives.
* It will be removed in the future.
*/
function createAsync(fn, options) {
let resource;
let prev = () => !resource || resource.state === "unresolved" ? undefined : resource.latest;
[resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, options);
const resultAccessor = () => resource();
if (options?.name) setFunctionName(resultAccessor, options.name);
Object.defineProperty(resultAccessor, "latest", {
get() {
return resource.latest;
}
});
return resultAccessor;
}
function createAsyncStore(fn, options = {}) {
let resource;
let prev = () => !resource || resource.state === "unresolved" ? undefined : unwrap(resource.latest);
[resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, {
...options,
storage: init => createDeepSignal(init, options.reconcile)
});
const resultAccessor = () => resource();
Object.defineProperty(resultAccessor, "latest", {
get() {
return resource.latest;
}
});
return resultAccessor;
}
function createDeepSignal(value, options) {
const [store, setStore] = createStore({
value: structuredClone(value)
});
return [() => store.value, v => {
typeof v === "function" && (v = v());
setStore("value", reconcile(structuredClone(v), options));
return store.value;
}];
}
// mock promise while hydrating to prevent fetching
class MockPromise {
static all() {
return new MockPromise();
}
static allSettled() {
return new MockPromise();
}
static any() {
return new MockPromise();
}
static race() {
return new MockPromise();
}
static reject() {
return new MockPromise();
}
static resolve() {
return new MockPromise();
}
catch() {
return new MockPromise();
}
then() {
return new MockPromise();
}
finally() {
return new MockPromise();
}
}
function subFetch(fn, prev) {
if (isServer || !sharedConfig.context) return fn(prev);
const ogFetch = fetch;
const ogPromise = Promise;
try {
window.fetch = () => new MockPromise();
Promise = MockPromise;
return fn(prev);
} finally {
window.fetch = ogFetch;
Promise = ogPromise;
}
}
function redirect(url, init = 302) {

@@ -1914,2 +1865,2 @@ let responseInit;

export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmissions };

@@ -1,2 +0,2 @@

import { isServer } from "solid-js/web";
import { isServer } from "@solidjs/web";
export function createBeforeLeave() {

@@ -3,0 +3,0 @@ let listeners = new Set();

@@ -13,4 +13,2 @@ import type { Component, JSX } from "solid-js";

transformUrl?: (url: string) => string;
/** @deprecated use rootPreload */
rootLoad?: RoutePreloadFunc;
};

@@ -25,5 +23,3 @@ export declare const createRouterComponent: (router: RouterIntegration) => (props: BaseRouterProps) => JSX.Element;

info?: Record<string, any>;
/** @deprecated use preload */
load?: RoutePreloadFunc<T>;
};
export declare const Route: <S extends string, T = unknown>(props: RouteProps<S, T>) => JSX.Element;
/*@refresh skip*/
import { children, createMemo, createRoot, getOwner, mergeProps, on, Show, untrack } from "solid-js";
import { getRequestEvent, isServer } from "solid-js/web";
import { children, createMemo, createRoot, getOwner, merge, untrack } from "solid-js";
import { getRequestEvent, isServer } from "@solidjs/web";
import { createBranches, createRouteContext, createRouterContext, getIntent, getRouteMatches, RouteContextObj, RouterContextObj, setInPreloadFn } from "../routing.js";

@@ -16,8 +16,8 @@ export const createRouterComponent = (router) => (props) => {

router.create && router.create(routerState);
return (<RouterContextObj.Provider value={routerState}>
<Root routerState={routerState} root={props.root} preload={props.rootPreload || props.rootLoad}>
return (<RouterContextObj value={routerState}>
<Root routerState={routerState} root={props.root} preload={props.rootPreload}>
{(context = getOwner()) && null}
<Routes routerState={routerState} branches={branches()}/>
</Root>
</RouterContextObj.Provider>);
</RouterContextObj>);
};

@@ -33,7 +33,9 @@ function Root(props) {

}));
return (<Show when={props.root} keyed fallback={props.children}>
{Root => (<Root params={params} location={location} data={data()}>
{props.children}
</Root>)}
</Show>);
const RootComp = props.root;
if (RootComp) {
return (<RootComp params={params} location={location} data={data()}>
{props.children}
</RootComp>);
}
return props.children;
}

@@ -59,7 +61,10 @@ function Routes(props) {

let root;
const routeStates = createMemo(on(props.routerState.matches, (nextMatches, prevMatches, prev) => {
let equal = prevMatches && nextMatches.length === prevMatches.length;
let prevMatches;
const routeStates = createMemo((prev) => {
const nextMatches = props.routerState.matches();
const previousMatches = prevMatches;
let equal = previousMatches && nextMatches.length === previousMatches.length;
const next = [];
for (let i = 0, len = nextMatches.length; i < len; i++) {
const prevMatch = prevMatches && prevMatches[i];
const prevMatch = previousMatches && previousMatches[i];
const nextMatch = nextMatches[i];

@@ -76,3 +81,3 @@ if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) {

disposers[i] = dispose;
next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()[i + 1]), () => {
next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => {
const routeMatches = props.routerState.matches();

@@ -86,17 +91,23 @@ return routeMatches[i] ?? routeMatches[0];

if (prev && equal) {
prevMatches = nextMatches;
return prev;
}
root = next[0];
prevMatches = nextMatches;
return next;
}));
}, undefined);
return createOutlet(() => routeStates() && root)();
}
const createOutlet = (child) => {
return () => (<Show when={child()} keyed>
{child => <RouteContextObj.Provider value={child}>{child.outlet()}</RouteContextObj.Provider>}
</Show>);
return () => {
const c = child();
if (c) {
return <RouteContextObj value={c}>{c.outlet()}</RouteContextObj>;
}
return undefined;
};
};
export const Route = (props) => {
const childRoutes = children(() => props.children);
return mergeProps(props, {
return merge(props, {
get children() {

@@ -103,0 +114,0 @@ return childRoutes();

@@ -1,2 +0,2 @@

import { isServer } from "solid-js/web";
import { isServer } from "@solidjs/web";
import { createRouter, scrollToHash, bindEvent } from "./createRouter.js";

@@ -3,0 +3,0 @@ import { StaticRouter } from "./StaticRouter.js";

@@ -1,2 +0,2 @@

import { getRequestEvent } from "solid-js/web";
import { getRequestEvent } from "@solidjs/web";
import { createRouterComponent } from "./components.jsx";

@@ -3,0 +3,0 @@ function getPath(url) {

@@ -42,4 +42,4 @@ import { JSX, Accessor } from "solid-js";

/**
* Retrieves signal that indicates whether the route is currently in a *Transition*.
* Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering.
* Retrieves a signal that indicates whether the router is currently processing a navigation.
* Useful for showing pending navigation state while the next route and its data settle.
*

@@ -46,0 +46,0 @@ * @example

@@ -1,4 +0,4 @@

import { runWithOwner, batch } from "solid-js";
import { createComponent, createContext, createMemo, createRenderEffect, createSignal, on, onCleanup, untrack, useContext, startTransition, resetErrorBoundaries } from "solid-js";
import { isServer, getRequestEvent } from "solid-js/web";
import { flush, runWithOwner } from "solid-js";
import { createComponent, createContext, createMemo, createSignal, onCleanup, untrack, useContext } from "solid-js";
import { isServer, getRequestEvent } from "@solidjs/web";
import { createBeforeLeave } from "./lifecycle.js";

@@ -10,5 +10,13 @@ import { mockBase, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";

export const RouteContextObj = createContext();
function useOptionalContext(context) {
try {
return useContext(context);
}
catch {
return undefined;
}
}
export const useRouter = () => invariant(useContext(RouterContextObj), "<A> and 'use' router primitives can be only used inside a Route.");
let TempRoute;
export const useRoute = () => TempRoute || useContext(RouteContextObj) || useRouter().base;
export const useRoute = () => TempRoute || useOptionalContext(RouteContextObj) || useRouter().base;
export const useResolvedPath = (path) => {

@@ -57,4 +65,4 @@ const route = useRoute();

/**
* Retrieves signal that indicates whether the route is currently in a *Transition*.
* Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering.
* Retrieves a signal that indicates whether the router is currently processing a navigation.
* Useful for showing pending navigation state while the next route and its data settle.
*

@@ -210,3 +218,3 @@ * @example

export function createRoutes(routeDef, base = "") {
const { component, preload, load, children, info } = routeDef;
const { component, preload, children, info } = routeDef;
const isLeaf = !children || (Array.isArray(children) && !children.length);

@@ -216,3 +224,3 @@ const shared = {

component,
preload: preload || load,
preload,
info

@@ -316,3 +324,3 @@ };

const key = () => "";
const queryFn = on(search, () => extractSearchParams(url()));
const queryFn = createMemo(() => extractSearchParams(url()));
return {

@@ -360,39 +368,28 @@ get pathname() {

}
const [isRouting, setIsRouting] = createSignal(false);
// Keep track of last target, so that last call to transition wins
const [isRouting, setIsRouting] = createSignal(false, { pureWrite: true });
// Navigate override written from event handlers.
const [navigateTarget, setNavigateTarget] = createSignal(undefined, {
pureWrite: true
});
// Keep track of last target, so that last call to navigate wins
let lastTransitionTarget;
// Transition the location to a new value
const transition = (newIntent, newTarget) => {
if (newTarget.value === reference() && newTarget.state === state())
return;
if (lastTransitionTarget === undefined)
setIsRouting(true);
intent = newIntent;
lastTransitionTarget = newTarget;
startTransition(() => {
if (lastTransitionTarget !== newTarget)
return;
setReference(lastTransitionTarget.value);
setState(lastTransitionTarget.state);
resetErrorBoundaries();
if (!isServer)
submissions[1](subs => subs.filter(s => s.pending));
}).finally(() => {
if (lastTransitionTarget !== newTarget)
return;
// Batch, in order for isRouting and final source update to happen together
batch(() => {
intent = undefined;
if (newIntent === "navigate")
navigateEnd(lastTransitionTarget);
setIsRouting(false);
lastTransitionTarget = undefined;
});
});
};
const [reference, setReference] = createSignal(source().value);
const [state, setState] = createSignal(source().state);
// source() remains canonical for native history changes; navigateTarget()
// temporarily overrides it for in-flight programmatic navigation.
const reference = createMemo(() => {
const nav = navigateTarget();
if (nav !== undefined)
return nav.value;
return source().value;
});
const state = createMemo(() => {
const nav = navigateTarget();
if (nav !== undefined)
return nav.state;
return source().state;
});
const location = createLocation(reference, state, utils.queryWrapper);
const referrers = [];
const submissions = createSignal(isServer ? initFromFlash() : []);
const submissions = createSignal(isServer ? initFromFlash() : [], {
pureWrite: true
});
const matches = createMemo(() => {

@@ -423,4 +420,2 @@ if (typeof options.transformUrl === "function") {

};
// Create a native transition, when source updates
createRenderEffect(on(source, source => transition("native", source), { defer: true }));
return {

@@ -480,6 +475,24 @@ base: baseRoute,

referrers.push({ value: current, replace, scroll, state: state() });
transition("navigate", {
const newTarget = {
value: resolvedTo,
state: nextState
});
};
if (lastTransitionTarget === undefined) {
setIsRouting(true);
flush();
}
intent = "navigate";
lastTransitionTarget = newTarget;
if (lastTransitionTarget === newTarget) {
setNavigateTarget({ ...lastTransitionTarget });
queueMicrotask(() => {
if (lastTransitionTarget !== newTarget)
return;
intent = undefined;
navigateEnd(lastTransitionTarget);
setNavigateTarget(undefined);
setIsRouting(false);
lastTransitionTarget = undefined;
});
}
}

@@ -491,3 +504,3 @@ }

// Workaround for vite issue (https://github.com/vitejs/vite/issues/3803)
route = route || useContext(RouteContextObj) || baseRoute;
route = route || useOptionalContext(RouteContextObj) || baseRoute;
return (to, options) => navigateFromRoute(route, to, options);

@@ -537,3 +550,11 @@ }

const e = getRequestEvent();
return (e && e.router && e.router.submission ? [e.router.submission] : []);
if (!(e && e.router && e.router.submission))
return [];
return [
{
...e.router.submission,
clear() { },
retry() { }
}
];
}

@@ -540,0 +561,0 @@ }

import type { Component, JSX, Signal } from "solid-js";
declare module "solid-js/web" {
declare module "@solidjs/web" {
interface RequestEvent {

@@ -15,2 +15,3 @@ response: {

result: any;
error?: any;
url: string;

@@ -82,4 +83,2 @@ };

info?: Record<string, any>;
/** @deprecated use preload */
load?: RoutePreloadFunc;
};

@@ -171,3 +170,2 @@ export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean);

readonly error: any;
readonly pending: boolean;
readonly url: string;

@@ -177,11 +175,2 @@ clear: () => void;

};
export type SubmissionStub = {
readonly input: undefined;
readonly result: undefined;
readonly error: undefined;
readonly pending: undefined;
readonly url: undefined;
clear: () => void;
retry: () => void;
};
export interface MaybePreloadableComponent extends Component {

@@ -201,5 +190,1 @@ preload?: () => void;

};
/** @deprecated */
export type RouteLoadFunc = RoutePreloadFunc;
/** @deprecated */
export type RouteLoadFuncArgs = RoutePreloadFuncArgs;

@@ -9,3 +9,3 @@ {

"license": "MIT",
"version": "0.16.1",
"version": "0.17.0-next.0",
"homepage": "https://github.com/solidjs/solid-router#readme",

@@ -33,2 +33,3 @@ "repository": {

"devDependencies": {
"@solidjs/web": "2.0.0-beta.3",
"@babel/core": "^7.26.0",

@@ -42,14 +43,15 @@ "@babel/preset-typescript": "^7.26.0",

"@types/node": "^22.10.0",
"babel-preset-solid": "^1.9.3",
"babel-preset-solid": "2.0.0-beta.3",
"jsdom": "^25.0.1",
"prettier": "^3.4.1",
"rollup": "^4.27.4",
"solid-js": "^1.9.3",
"solid-js": "2.0.0-beta.3",
"typescript": "^5.7.2",
"vite": "^6.0.0",
"vite-plugin-solid": "^2.11.0",
"vite-plugin-solid": "3.0.0-next.2",
"vitest": "^2.1.6"
},
"peerDependencies": {
"solid-js": "^1.8.6"
"@solidjs/web": "^2.0.0-beta.3",
"solid-js": "^2.0.0-beta.3"
},

@@ -56,0 +58,0 @@ "scripts": {

+85
-79

@@ -67,3 +67,3 @@ [![Banner](https://assets.solidjs.com/banner?project=Router&type=core)](https://github.com/solidjs)

```jsx
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router } from "@solidjs/router";

@@ -83,3 +83,3 @@

```jsx
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router, Route } from "@solidjs/router";

@@ -106,3 +106,3 @@

```jsx
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router, Route } from "@solidjs/router";

@@ -136,3 +136,3 @@

```jsx
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router, Route } from "@solidjs/router";

@@ -169,3 +169,3 @@

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router, Route } from "@solidjs/router";

@@ -200,3 +200,3 @@

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router, Route } from "@solidjs/router";

@@ -235,3 +235,3 @@

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router, Route } from "@solidjs/router";

@@ -275,3 +275,3 @@

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router, Route } from "@solidjs/router";

@@ -509,5 +509,6 @@ import type { MatchFilters } from "@solidjs/router";

import { getUser } from ... // the query function
import { createMemo } from "solid-js";
export default function User(props) {
const user = createAsync(() => getUser(props.params.id));
const user = createMemo(() => getUser(props.params.id));
return <h1>{user().name}</h1>;

@@ -530,31 +531,20 @@ }

### `createAsync`
### Async reads in Solid 2
This is light wrapper over `createResource` that aims to serve as stand-in for a future primitive we intend to bring to Solid core in 2.0. It is a simpler async primitive where the function tracks like `createMemo` and it expects a promise back that it turns into a Signal. Reading it before it is ready causes Suspense/Transitions to trigger.
On this Solid 2 branch, `query()` results are meant to be consumed directly with Solid primitives like `createMemo` and `createProjection`.
```jsx
const user = createAsync((currentValue) => getUser(params.id));
const user = createMemo(() => getUser(params.id));
return <h1>{user().name}</h1>;
```
It also preserves `latest` field from `createResource`. Note that it will be removed in the future.
For object-shaped data where you want a deeply reactive result, use `createProjection`.
```jsx
const user = createAsync((currentValue) => getUser(params.id));
return <h1>{user.latest.name}</h1>;
const todos = createProjection(() => getTodos(), []);
```
Using `query` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
### `createAsyncStore`
Similar to `createAsync` except it uses a deeply reactive store. Perfect for applying fine-grained changes to large model data that updates.
It also supports `latest` field which will be removed in the future.
```jsx
const todos = createAsyncStore(() => getTodos());
```
### `action`
Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response helpers can be found below.
Router `action()` is the router-aware mutation wrapper for Solid 2. It keeps form submission, redirects, and invalidation wired into the router while letting you compose optimistic UI with Solid's built-in primitives.

@@ -579,2 +569,25 @@ ```jsx

For optimistic updates, use Solid's optimistic primitives for the rendered state and attach owner-scoped submit hooks to the router action:
```jsx
import { createOptimisticStore } from "solid-js";
import { action, query } from "@solidjs/router";
const getTodos = query(async () => fetchTodos(), "todos");
const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
const addTodo = action(async (todo) => {
await saveTodo(todo);
return { ok: true, todo };
}, "add-todo").onSubmit(todo => {
setTodos(items => {
items.push({ ...todo, pending: true });
});
});
```
`myAction.onSubmit(...)` registers a listener for that action in the current reactive owner. Multiple components can register hooks against the same action, and those hooks are automatically removed when their owner is disposed. `myAction.onSettled(...)` works the same way for observing completed submissions.
The preferred pattern is for actions to return values and let the client interpret the result. Throwing errors is still supported, but `Submission.error` is mainly an escape hatch for that legacy style.
Sometimes it might be easier to deal with typed data instead of `FormData` and adding additional hidden fields. For that reason Actions have a with method. That works similar to `bind` which applies the arguments in order.

@@ -606,3 +619,3 @@

Actions also take a second argument which can be the name or an option object with `name` and `onComplete`. `name` is used to identify SSR actions that aren't server functions (see note below). `onComplete` allows you to configure behavior when `action`s complete. Keep in mind `onComplete` does not work when JavaScript is disabled.
Actions also take a second argument which can be the name or an option object with `name`. `name` is used to identify SSR actions that aren't server functions (see note below).

@@ -629,5 +642,5 @@ #### Notes on `<form>` implementation and SSR

### `useSubmission`/`useSubmissions`
### `useSubmissions`
Are used to injecting the optimistic updates while actions are in flight. They either return a single Submission(latest) or all that match with an optional filter function.
This returns settled submission records for an action. It is useful for reading completed results, clearing old submissions, retrying a prior submission, or replaying settled errors. It is not the optimistic state layer.

@@ -638,3 +651,3 @@ ```jsx

readonly result?: U;
readonly pending: boolean;
readonly error: any;
readonly url: string;

@@ -646,5 +659,7 @@ clear: () => void;

const submissions = useSubmissions(action, (input) => filter(input));
const submission = useSubmission(action, (input) => filter(input));
const latestSubmission = submissions.at(-1);
```
Use Solid's `createOptimistic` or `createOptimisticStore` for in-flight UI, and use submissions as the durable settled record layer.
### Response Helpers

@@ -688,3 +703,3 @@

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router } from "@solidjs/router";

@@ -732,3 +747,3 @@

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { render } from "@solidjs/web";
import { Router } from "@solidjs/router";

@@ -771,3 +786,3 @@

```jsx
import { isServer } from "solid-js/web";
import { isServer } from "@solidjs/web";
import { Router } from "@solidjs/router";

@@ -905,3 +920,3 @@

Retrieves signal that indicates whether the route is currently in a Transition. Useful for showing stale/pending state when the route resolution is Suspended during concurrent rendering.
Retrieves a signal that indicates whether the router is currently processing a navigation. Useful for showing pending navigation state while the next route and its data settle.

@@ -912,3 +927,3 @@ ```js

return (
<div classList={{ "grey-out": isRouting() }}>
<div class={{ "grey-out": isRouting() }}>
<MyAwesomeContent />

@@ -926,3 +941,3 @@ </div>

return <div classList={{ active: Boolean(match()) }} />;
return <div class={{ active: Boolean(match()) }} />;
```

@@ -982,58 +997,49 @@

## Migrations from 0.9.x
## Migration from 0.16.x
v0.10.0 brings some big changes to support the future of routing including Islands/Partial Hydration hybrid solutions. Most notably there is no Context API available in non-hydrating parts of the application.
This branch is the Solid 2 migration. Most route configuration stays the same, but the data APIs and recommended async patterns have changed.
The biggest changes are around removed APIs that need to be replaced.
### Async reads move to Solid 2 primitives
### `<Outlet>`, `<Routes>`, `useRoutes`
`createAsync` and `createAsyncStore` are gone. Read query results with Solid 2 primitives like `createMemo`, `createProjection`, `createOptimistic`, and `createOptimisticStore`.
This is no longer used and instead will use `props.children` passed from into the page components for outlets. This keeps the outlet directly passed from its page and avoids oddness of trying to use context across Islands boundaries. Nested `<Routes>` components inherently cause waterfalls and are `<Outlets>` themselves so they have the same concerns.
```jsx
const user = createMemo(() => getUser(params.id));
const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
```
Keep in mind no `<Routes>` means the `<Router>` API is different. The `<Router>` acts as the `<Routes>` component and its children can only be `<Route>` components. Your top-level layout should go in the root prop of the router [as shown above](#configure-your-routes)
### `query()` stays the source of truth
## `element` prop removed from `Route`
Continue using `query()` for cached reads and invalidation, but consume the results directly through Solid 2's async primitives instead of router-specific wrappers.
Related without Outlet component it has to be passed in manually. At which point the `element` prop has less value. Removing the second way to define route components to reduce confusion and edge cases.
### `action()` lifecycle hooks changed
### `data` functions & `useRouteData`
The action API is now centered around instance methods:
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/query 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.
```jsx
const saveTodo = action(async (todo) => {
await api.saveTodo(todo);
return { ok: true, todo };
}, "save-todo")
.onSubmit(todo => {
// optimistic write
})
.onSettled(submission => {
// observe settled result or retry state
});
```
That being said you can reproduce the old pattern largely by turning off preloads at the router level and then injecting your own Context:
- Use `onSubmit(...)` for owner-scoped optimistic/pre-submit work.
- Use `onSettled(...)` for owner-scoped observation of completed submissions.
- Use returned values for expected application-level results. Thrown errors are still captured on `Submission.error` when something fails unexpectedly.
```js
import { lazy } from "solid-js";
import { Route } from "@solidjs/router";
### `useSubmissions()` is the submission API
const User = lazy(() => import("./pages/users/[id].js"));
Submissions are now settled history, not in-flight mutation state. Read them through `useSubmissions()` and select the latest entry with `at(-1)` when needed.
// preload function
function preloadUser({ params, location }) {
const [user] = createResource(() => params.id, fetchUser);
return user;
}
// Pass it in the route definition
<Router preload={false}>
<Route path="/users/:id" component={User} preload={preloadUser} />
</Router>;
```jsx
const submissions = useSubmissions(saveTodo);
const latestSubmission = submissions.at(-1);
```
And then in your component taking the page props and putting them in a Context.
```js
function User(props) {
<UserContext.Provider value={props.data}>
{/* my component content */}
</UserContext.Provider>;
}
// Somewhere else
function UserDetails() {
const user = useContext(UserContext);
// render stuff
}
```
## SPAs in Deployed Environments

@@ -1040,0 +1046,0 @@

import { type ReconcileOptions } from "solid-js/store";
/**
* As `createAsync` and `createAsyncStore` are wrappers for `createResource`,
* this type allows to support `latest` field for these primitives.
* It will be removed in the future.
*/
export type AccessorWithLatest<T> = {
(): T;
latest: T;
};
export declare function createAsync<T>(fn: (prev: T) => Promise<T>, options: {
name?: string;
initialValue: T;
deferStream?: boolean;
}): AccessorWithLatest<T>;
export declare function createAsync<T>(fn: (prev: T | undefined) => Promise<T>, options?: {
name?: string;
initialValue?: T;
deferStream?: boolean;
}): AccessorWithLatest<T | undefined>;
export declare function createAsyncStore<T>(fn: (prev: T) => Promise<T>, options: {
name?: string;
initialValue: T;
deferStream?: boolean;
reconcile?: ReconcileOptions;
}): AccessorWithLatest<T>;
export declare function createAsyncStore<T>(fn: (prev: T | undefined) => Promise<T>, options?: {
name?: string;
initialValue?: T;
deferStream?: boolean;
reconcile?: ReconcileOptions;
}): AccessorWithLatest<T | undefined>;
/**
* This is mock of the eventual Solid 2.0 primitive. It is not fully featured.
*/
import { createResource, sharedConfig, untrack, catchError } from "solid-js";
import { createStore, reconcile, unwrap } from "solid-js/store";
import { isServer } from "solid-js/web";
import { setFunctionName } from "../utils.js";
export function createAsync(fn, options) {
let resource;
let prev = () => !resource || resource.state === "unresolved" ? undefined : resource.latest;
[resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, options);
const resultAccessor = (() => resource());
if (options?.name)
setFunctionName(resultAccessor, options.name);
Object.defineProperty(resultAccessor, "latest", {
get() {
return resource.latest;
}
});
return resultAccessor;
}
export function createAsyncStore(fn, options = {}) {
let resource;
let prev = () => !resource || resource.state === "unresolved"
? undefined
: unwrap(resource.latest);
[resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, {
...options,
storage: (init) => createDeepSignal(init, options.reconcile)
});
const resultAccessor = (() => resource());
Object.defineProperty(resultAccessor, "latest", {
get() {
return resource.latest;
}
});
return resultAccessor;
}
function createDeepSignal(value, options) {
const [store, setStore] = createStore({
value: structuredClone(value)
});
return [
() => store.value,
(v) => {
typeof v === "function" && (v = v());
setStore("value", reconcile(structuredClone(v), options));
return store.value;
}
];
}
// mock promise while hydrating to prevent fetching
class MockPromise {
static all() {
return new MockPromise();
}
static allSettled() {
return new MockPromise();
}
static any() {
return new MockPromise();
}
static race() {
return new MockPromise();
}
static reject() {
return new MockPromise();
}
static resolve() {
return new MockPromise();
}
catch() {
return new MockPromise();
}
then() {
return new MockPromise();
}
finally() {
return new MockPromise();
}
}
function subFetch(fn, prev) {
if (isServer || !sharedConfig.context)
return fn(prev);
const ogFetch = fetch;
const ogPromise = Promise;
try {
window.fetch = () => new MockPromise();
Promise = MockPromise;
return fn(prev);
}
finally {
window.fetch = ogFetch;
Promise = ogPromise;
}
}