@octanejs/tanstack-router 0.1.2
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/LICENSE +21 -0
- package/README.md +94 -0
- package/package.json +47 -0
- package/src/Await.tsrx +21 -0
- package/src/Await.tsrx.d.ts +6 -0
- package/src/CatchBoundary.tsrx +91 -0
- package/src/CatchBoundary.tsrx.d.ts +8 -0
- package/src/ClientOnly.tsrx +21 -0
- package/src/ClientOnly.tsrx.d.ts +3 -0
- package/src/Link.tsrx +32 -0
- package/src/Link.tsrx.d.ts +4 -0
- package/src/Match.tsrx +204 -0
- package/src/Match.tsrx.d.ts +2 -0
- package/src/MatchRoute.tsrx +32 -0
- package/src/MatchRoute.tsrx.d.ts +3 -0
- package/src/Matches.tsrx +58 -0
- package/src/Matches.tsrx.d.ts +2 -0
- package/src/Navigate.tsrx +14 -0
- package/src/Navigate.tsrx.d.ts +2 -0
- package/src/Outlet.tsrx +49 -0
- package/src/Outlet.tsrx.d.ts +2 -0
- package/src/RouteNotFound.tsrx +23 -0
- package/src/RouterProvider.tsrx +37 -0
- package/src/RouterProvider.tsrx.d.ts +8 -0
- package/src/SafeFragment.tsrx +12 -0
- package/src/SafeFragment.tsrx.d.ts +2 -0
- package/src/ScrollRestoration.tsrx +16 -0
- package/src/ScrollRestoration.tsrx.d.ts +2 -0
- package/src/Transitioner.tsrx +126 -0
- package/src/context.ts +27 -0
- package/src/history.ts +10 -0
- package/src/hooks.ts +225 -0
- package/src/index.ts +91 -0
- package/src/internal.ts +31 -0
- package/src/lazyRouteComponent.ts +56 -0
- package/src/link.ts +383 -0
- package/src/not-found.tsrx +39 -0
- package/src/not-found.tsrx.d.ts +7 -0
- package/src/route.ts +147 -0
- package/src/router.ts +63 -0
- package/src/useAwaited.ts +9 -0
- package/src/useBlocker.tsrx +136 -0
- package/src/useBlocker.tsrx.d.ts +3 -0
- package/src/useElementScrollRestoration.ts +12 -0
- package/src/useRouterState.ts +17 -0
- package/src/useStore.ts +46 -0
- package/src/utils.ts +18 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Imperative navigation as a component: navigates once on mount, renders nothing.
|
|
2
|
+
import { useLayoutEffect, useRef } from 'octane';
|
|
3
|
+
import { useNavigate } from './hooks.ts';
|
|
4
|
+
|
|
5
|
+
export function Navigate(props) @{
|
|
6
|
+
const navigate = useNavigate();
|
|
7
|
+
const done = useRef(false);
|
|
8
|
+
|
|
9
|
+
useLayoutEffect(() => {
|
|
10
|
+
if (done.current) return;
|
|
11
|
+
done.current = true;
|
|
12
|
+
navigate(props);
|
|
13
|
+
}, []);
|
|
14
|
+
}
|
package/src/Outlet.tsrx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Renders the child match below the current one. Reads the current match id from
|
|
2
|
+
// `matchContext`, finds the NEXT id in the match-id chain, and renders its `<Match/>`
|
|
3
|
+
// — the pull-based descent that replaces a top-down state diff. Renders nothing at
|
|
4
|
+
// a leaf.
|
|
5
|
+
//
|
|
6
|
+
// Not-found (react-router's Outlet, same order): when the URL matched no route,
|
|
7
|
+
// router-core flags ONE match `globalNotFound` — with the default
|
|
8
|
+
// `notFoundMode: 'fuzzy'` the deepest fuzzy-matched route that has children, with
|
|
9
|
+
// `notFoundMode: 'root'` the root. That match still renders its own component
|
|
10
|
+
// (the layout), and its `<Outlet/>` renders the not-found UI INSTEAD of a child
|
|
11
|
+
// match — so the 404 lands inside the layout chrome.
|
|
12
|
+
import { useContext, createElement, Suspense } from 'octane';
|
|
13
|
+
import { rootRouteId } from '@tanstack/router-core';
|
|
14
|
+
import { useStore } from './useStore.ts';
|
|
15
|
+
import { useRouter, matchContext } from './context.ts';
|
|
16
|
+
import { Match } from './Match.tsrx';
|
|
17
|
+
import { RouteNotFound } from './RouteNotFound.tsrx';
|
|
18
|
+
import { SafeFragment } from './SafeFragment.tsrx';
|
|
19
|
+
|
|
20
|
+
export function Outlet() @{
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
const parentId = useContext(matchContext);
|
|
23
|
+
const parentStore = router.stores.matchStores.get(parentId);
|
|
24
|
+
const parentRouteId = useStore(parentStore, (m) => m?.routeId);
|
|
25
|
+
const globalNotFound = useStore(parentStore, (m) => m?.globalNotFound ?? false);
|
|
26
|
+
const childId = useStore(router.stores.matchesId, (ids) => {
|
|
27
|
+
const i = ids.indexOf(parentId);
|
|
28
|
+
return i >= 0 ? ids[i + 1] : undefined;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// The root route's outlet wraps the first real match in a Suspense boundary
|
|
32
|
+
// whose fallback is the router's defaultPendingComponent (react-router's
|
|
33
|
+
// Outlet does the same) — the outermost pending UI for the initial load.
|
|
34
|
+
const DefaultPending = router.options.defaultPendingComponent;
|
|
35
|
+
const RootSuspense =
|
|
36
|
+
parentRouteId === rootRouteId ? Suspense : SafeFragment;
|
|
37
|
+
const pendingElement =
|
|
38
|
+
DefaultPending ? createElement(DefaultPending, {}) : null;
|
|
39
|
+
|
|
40
|
+
@if (globalNotFound) {
|
|
41
|
+
<RouteNotFound routeId={parentRouteId} />
|
|
42
|
+
} @else {
|
|
43
|
+
@if (childId) {
|
|
44
|
+
<RootSuspense fallback={pendingElement}>
|
|
45
|
+
<Match matchId={childId} />
|
|
46
|
+
</RootSuspense>
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Renders a route's not-found UI — the port of react-router's
|
|
2
|
+
// `renderRouteNotFound` (+ its `DefaultGlobalNotFound`). Resolution order matches
|
|
3
|
+
// upstream: the route's own `notFoundComponent`, else the router-level
|
|
4
|
+
// `defaultNotFoundComponent`, else TanStack's generic `<p>Not Found</p>` (the
|
|
5
|
+
// upstream dev-only console.warn is not ported — repo policy: functional outcomes
|
|
6
|
+
// only). Rendered from two places, mirroring react-router: `<Outlet/>` when its
|
|
7
|
+
// match is flagged `globalNotFound` (the URL matched no route), and `<Match/>`
|
|
8
|
+
// when the match resolved with `status === 'notFound'` (a loader threw
|
|
9
|
+
// `notFound()`). Upstream spreads the NotFoundError onto the component, so a
|
|
10
|
+
// `notFound({ data })` payload arrives as the `data` prop.
|
|
11
|
+
import { useRouter } from './context.ts';
|
|
12
|
+
|
|
13
|
+
export function RouteNotFound(props) @{
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const route = router.routesById[props.routeId];
|
|
16
|
+
const NotFound = route.options.notFoundComponent ?? router.options.defaultNotFoundComponent;
|
|
17
|
+
|
|
18
|
+
@if (NotFound) {
|
|
19
|
+
<NotFound {...props.error ?? {}} />
|
|
20
|
+
} @else {
|
|
21
|
+
<p>Not Found</p>
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Places the router into context and renders the active match tree. Mirrors
|
|
2
|
+
// react-router's RouterProvider.tsx: `RouterContextProvider` is the low-level
|
|
3
|
+
// provider — it forwards any extra props into `router.update()` (so
|
|
4
|
+
// `<RouterProvider router={r} defaultPreload="intent">` reconfigures the
|
|
5
|
+
// instance), wraps in `router.options.Wrap` when configured, and provides the
|
|
6
|
+
// router; `RouterProvider` renders `<Matches/>` inside it.
|
|
7
|
+
import { hasKeys } from '@tanstack/router-core';
|
|
8
|
+
import { routerContext } from './context.ts';
|
|
9
|
+
import { Matches } from './Matches.tsrx';
|
|
10
|
+
import { SafeFragment } from './SafeFragment.tsrx';
|
|
11
|
+
|
|
12
|
+
export function RouterContextProvider(props) @{
|
|
13
|
+
const { router, children, ...rest } = props;
|
|
14
|
+
if (hasKeys(rest)) {
|
|
15
|
+
router.update({
|
|
16
|
+
...router.options,
|
|
17
|
+
...rest,
|
|
18
|
+
context: {
|
|
19
|
+
...router.options.context,
|
|
20
|
+
...rest.context,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const Wrap = router.options.Wrap ?? SafeFragment;
|
|
25
|
+
|
|
26
|
+
<Wrap>
|
|
27
|
+
<routerContext.Provider value={router}>{children}</routerContext.Provider>
|
|
28
|
+
</Wrap>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function RouterProvider(props) @{
|
|
32
|
+
const { router, ...rest } = props;
|
|
33
|
+
|
|
34
|
+
<RouterContextProvider router={router} {...rest}>
|
|
35
|
+
<Matches />
|
|
36
|
+
</RouterContextProvider>
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Type declaration for the .tsrx component (resolved by relative path).
|
|
2
|
+
import type { AnyRouter } from '@tanstack/router-core';
|
|
3
|
+
|
|
4
|
+
export declare const RouterProvider: (props: { router: AnyRouter; children?: unknown }) => unknown;
|
|
5
|
+
export declare const RouterContextProvider: (props: {
|
|
6
|
+
router: import('@tanstack/router-core').AnyRouter;
|
|
7
|
+
children?: unknown;
|
|
8
|
+
}) => unknown;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Pass-through wrapper used wherever react-router conditionally omits a boundary
|
|
2
|
+
// (Suspense / CatchBoundary / CatchNotFound / shellComponent). Rendering through
|
|
3
|
+
// SafeFragment instead of conditionally nesting the template keeps the boundary
|
|
4
|
+
// slot's component identity dynamic (`<Wrap>` where Wrap is Suspense or this) and
|
|
5
|
+
// — crucially — means a route WITHOUT a pendingComponent/errorComponent does not
|
|
6
|
+
// create a boundary at all, so suspensions and errors bubble to the nearest
|
|
7
|
+
// ancestor that has one (react-router parity).
|
|
8
|
+
export function SafeFragment(props) @{
|
|
9
|
+
<>
|
|
10
|
+
{props.children}
|
|
11
|
+
</>
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Restores scroll position across navigations. router-core's
|
|
2
|
+
// `setupScrollRestoration` does all the work (save on scroll keyed by location,
|
|
3
|
+
// restore on navigation) — the component just wires it on mount and renders nothing.
|
|
4
|
+
// You can also enable it via `createRouter({ scrollRestoration: true })` (handled in
|
|
5
|
+
// Matches); use one or the other.
|
|
6
|
+
import { useLayoutEffect } from 'octane';
|
|
7
|
+
import { setupScrollRestoration } from '@tanstack/router-core';
|
|
8
|
+
import { useRouter } from './context.ts';
|
|
9
|
+
|
|
10
|
+
export function ScrollRestoration() @{
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
setupScrollRestoration(router, true);
|
|
14
|
+
}, [router]);
|
|
15
|
+
<></>
|
|
16
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// The navigation engine (renders nothing) — port of react-router's Transitioner.
|
|
2
|
+
// It (1) supplies `router.startTransition` so every navigation state update rides
|
|
3
|
+
// an octane transition (concurrent navigation: the current page holds while the
|
|
4
|
+
// next route suspends), (2) subscribes to history so back/forward and `Link`
|
|
5
|
+
// clicks reload the matches, (3) kicks off the initial `router.load()` (skipped
|
|
6
|
+
// when hydrating SSR — ssr-client triggers it) and commits a canonical-URL
|
|
7
|
+
// replace when the mounted URL isn't in canonical form, and (4) drives the router
|
|
8
|
+
// EVENT LIFECYCLE: `onLoad` when a load settles, `onBeforeRouteMount` /
|
|
9
|
+
// `onResolved` when all pending work (load + transition + pending matches)
|
|
10
|
+
// drains, then commits `status: 'idle'` + `resolvedLocation`. router-core's
|
|
11
|
+
// scroll restoration, devtools, and `router.subscribe` consumers all key off
|
|
12
|
+
// these events, and `useRouterState` selectors on `resolvedLocation` depend on
|
|
13
|
+
// the commit.
|
|
14
|
+
import { useEffect, useLayoutEffect, useRef, useState, startTransition } from 'octane';
|
|
15
|
+
import { batch } from '@tanstack/store';
|
|
16
|
+
import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core';
|
|
17
|
+
import { useRouter } from './context.ts';
|
|
18
|
+
import { useStore } from './useStore.ts';
|
|
19
|
+
import { usePrevious } from './utils.ts';
|
|
20
|
+
|
|
21
|
+
export function Transitioner() @{
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const mountLoadForRouter = useRef({ router, mounted: false });
|
|
24
|
+
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
25
|
+
|
|
26
|
+
const isLoading = useStore(router.stores.isLoading, (v) => v);
|
|
27
|
+
const hasPending = useStore(router.stores.hasPending, (v) => v);
|
|
28
|
+
|
|
29
|
+
const previousIsLoading = usePrevious(isLoading);
|
|
30
|
+
const isAnyPending = isLoading || isTransitioning || hasPending;
|
|
31
|
+
const previousIsAnyPending = usePrevious(isAnyPending);
|
|
32
|
+
const isPagePending = isLoading || hasPending;
|
|
33
|
+
const previousIsPagePending = usePrevious(isPagePending);
|
|
34
|
+
|
|
35
|
+
router.startTransition = (fn) => {
|
|
36
|
+
setIsTransitioning(true);
|
|
37
|
+
startTransition(() => {
|
|
38
|
+
fn();
|
|
39
|
+
setIsTransitioning(false);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Subscribe to location changes and load the new location. On mount, also
|
|
44
|
+
// verify the current URL is in canonical form (search serialization,
|
|
45
|
+
// trailing slash) and replace it if not — mirroring the server-side redirect
|
|
46
|
+
// check in router.beforeLoad.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const unsub = router.history.subscribe(() => router.load());
|
|
49
|
+
|
|
50
|
+
const nextLocation = router.buildLocation({
|
|
51
|
+
to: router.latestLocation.pathname,
|
|
52
|
+
search: true,
|
|
53
|
+
params: true,
|
|
54
|
+
hash: true,
|
|
55
|
+
state: true,
|
|
56
|
+
_includeValidateSearch: true,
|
|
57
|
+
});
|
|
58
|
+
if (
|
|
59
|
+
trimPathRight(router.latestLocation.publicHref) !== trimPathRight(nextLocation.publicHref)
|
|
60
|
+
) {
|
|
61
|
+
router.commitLocation({ ...nextLocation, replace: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return () => unsub();
|
|
65
|
+
}, [router, router.history]);
|
|
66
|
+
|
|
67
|
+
// Initial load. Skipped when hydrating from SSR (ssr-client triggers it) and
|
|
68
|
+
// on re-runs for the same router instance.
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
if (
|
|
71
|
+
typeof window !== 'undefined' && router.ssr ||
|
|
72
|
+
mountLoadForRouter.current.router === router && mountLoadForRouter.current.mounted
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
mountLoadForRouter.current = { router, mounted: true };
|
|
77
|
+
router.load().catch((err: unknown) => {
|
|
78
|
+
console.error(err);
|
|
79
|
+
});
|
|
80
|
+
}, [router]);
|
|
81
|
+
|
|
82
|
+
// The router was loading and now it's not — the new matches are in state.
|
|
83
|
+
useLayoutEffect(() => {
|
|
84
|
+
if (previousIsLoading && !isLoading) {
|
|
85
|
+
router.emit({
|
|
86
|
+
type: 'onLoad',
|
|
87
|
+
...getLocationChangeInfo(
|
|
88
|
+
router.stores.location.get(),
|
|
89
|
+
router.stores.resolvedLocation.get(),
|
|
90
|
+
),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}, [previousIsLoading, router, isLoading]);
|
|
94
|
+
|
|
95
|
+
useLayoutEffect(() => {
|
|
96
|
+
if (previousIsPagePending && !isPagePending) {
|
|
97
|
+
router.emit({
|
|
98
|
+
type: 'onBeforeRouteMount',
|
|
99
|
+
...getLocationChangeInfo(
|
|
100
|
+
router.stores.location.get(),
|
|
101
|
+
router.stores.resolvedLocation.get(),
|
|
102
|
+
),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}, [isPagePending, previousIsPagePending, router]);
|
|
106
|
+
|
|
107
|
+
// Everything pending has drained — resolve the navigation: emit onResolved and
|
|
108
|
+
// commit status idle + resolvedLocation (what `onRendered` and scroll
|
|
109
|
+
// restoration key off).
|
|
110
|
+
useLayoutEffect(() => {
|
|
111
|
+
if (previousIsAnyPending && !isAnyPending) {
|
|
112
|
+
const changeInfo = getLocationChangeInfo(
|
|
113
|
+
router.stores.location.get(),
|
|
114
|
+
router.stores.resolvedLocation.get(),
|
|
115
|
+
);
|
|
116
|
+
router.emit({ type: 'onResolved', ...changeInfo });
|
|
117
|
+
|
|
118
|
+
batch(() => {
|
|
119
|
+
router.stores.status.set('idle');
|
|
120
|
+
router.stores.resolvedLocation.set(router.stores.location.get());
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}, [isAnyPending, previousIsAnyPending, router]);
|
|
124
|
+
|
|
125
|
+
<></>
|
|
126
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Router + match contexts. `routerContext` carries the Router instance (provided
|
|
2
|
+
// by RouterProvider, read by every hook). `matchContext` carries the current
|
|
3
|
+
// match id down the render tree so `Outlet` can find the NEXT match to render —
|
|
4
|
+
// the pull-based chaining that replaces a top-down state diff.
|
|
5
|
+
import { createContext, useContext } from 'octane';
|
|
6
|
+
import type { AnyRouter } from '@tanstack/router-core';
|
|
7
|
+
|
|
8
|
+
export const routerContext = createContext<AnyRouter | undefined>(undefined);
|
|
9
|
+
export const getRouterContext = (): typeof routerContext => routerContext;
|
|
10
|
+
|
|
11
|
+
// The id of the nearest rendered match (undefined above the first match).
|
|
12
|
+
export const matchContext = createContext<string | undefined>(undefined);
|
|
13
|
+
|
|
14
|
+
// Resolve the active router: an explicitly-passed one wins, else the context.
|
|
15
|
+
// `useContext` is keyed by context identity (not a per-call-site slot), so it's
|
|
16
|
+
// safe to call from this binding code without a slot.
|
|
17
|
+
export function useRouter(...args: unknown[]): AnyRouter {
|
|
18
|
+
const opts = (args.length && typeof args[0] !== 'symbol' ? args[0] : undefined) as
|
|
19
|
+
| { router?: AnyRouter; warn?: boolean }
|
|
20
|
+
| undefined;
|
|
21
|
+
const ctx = useContext(routerContext);
|
|
22
|
+
const router = opts?.router ?? ctx;
|
|
23
|
+
if (!router && opts?.warn !== false) {
|
|
24
|
+
throw new Error('useRouter must be used inside a <RouterProvider> component!');
|
|
25
|
+
}
|
|
26
|
+
return router as AnyRouter;
|
|
27
|
+
}
|
package/src/history.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// `@tanstack/history` is a separate framework-agnostic dependency (the browser/
|
|
2
|
+
// hash/memory history abstractions). react-router re-exports it from `./history`;
|
|
3
|
+
// we mirror that so `@octanejs/tanstack-router/history` and the bare entry both resolve it.
|
|
4
|
+
export {
|
|
5
|
+
createHistory,
|
|
6
|
+
createBrowserHistory,
|
|
7
|
+
createHashHistory,
|
|
8
|
+
createMemoryHistory,
|
|
9
|
+
} from '@tanstack/history';
|
|
10
|
+
export type * from '@tanstack/history';
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// The read hooks — ports of react-router's useMatch.tsx / useParams / useSearch /
|
|
2
|
+
// useLoaderData / useLoaderDeps / useRouteContext / useNavigate / useCanGoBack /
|
|
3
|
+
// Matches.tsx (useMatches / useParentMatches / useChildMatches). Everything match-
|
|
4
|
+
// shaped funnels through `useMatch`, which subscribes to ONE match store:
|
|
5
|
+
// - `from` given → `router.stores.getRouteMatchStore(from)` (a cached computed
|
|
6
|
+
// that resolves a routeId to its current match);
|
|
7
|
+
// - no `from` → the NEAREST match via `matchContext` (the match id the enclosing
|
|
8
|
+
// `<Match>` provided) — NOT the leaf match.
|
|
9
|
+
// A missing match throws unless `shouldThrow: false` (upstream invariant).
|
|
10
|
+
// Selectors run through `useStructuralSharing` (replaceEqualDeep against the
|
|
11
|
+
// previous selection when `structuralSharing ?? defaultStructuralSharing`).
|
|
12
|
+
import { useContext, useRef, useCallback } from 'octane';
|
|
13
|
+
import { replaceEqualDeep } from '@tanstack/router-core';
|
|
14
|
+
import { useRouter, matchContext } from './context';
|
|
15
|
+
import { useStore } from './useStore';
|
|
16
|
+
import { splitSlot, subSlot } from './internal';
|
|
17
|
+
|
|
18
|
+
// Sentinel store + selection for "no match at this id" (upstream's dummyStore).
|
|
19
|
+
const dummyStore = {
|
|
20
|
+
get() {},
|
|
21
|
+
subscribe() {
|
|
22
|
+
return { unsubscribe() {} };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Selector wrapper honoring structural sharing: when enabled, the selection is
|
|
27
|
+
// replaceEqualDeep'd against the previous one so deep-equal slices keep their
|
|
28
|
+
// reference (no re-render). Port of react-router's useStructuralSharing.
|
|
29
|
+
function useStructuralSharing(opts: any, router: any, slot: symbol | undefined) {
|
|
30
|
+
const previousResult = useRef<any>(undefined, subSlot(slot, 'ss'));
|
|
31
|
+
return (slice: any) => {
|
|
32
|
+
const selected = opts?.select ? opts.select(slice) : slice;
|
|
33
|
+
if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) {
|
|
34
|
+
return (previousResult.current = replaceEqualDeep(previousResult.current, selected));
|
|
35
|
+
}
|
|
36
|
+
return selected;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useMatch(...args: any[]): any {
|
|
41
|
+
const [user, slot] = splitSlot(args);
|
|
42
|
+
const opts = user[0] ?? {};
|
|
43
|
+
const router = useRouter();
|
|
44
|
+
// octane has no rules of hooks, so the nearest-match context is read
|
|
45
|
+
// unconditionally (upstream reads a dummy context when `from` is given).
|
|
46
|
+
const nearestMatchId = useContext(matchContext);
|
|
47
|
+
const matchStore = opts.from
|
|
48
|
+
? router.stores.getRouteMatchStore(opts.from)
|
|
49
|
+
: router.stores.matchStores.get(nearestMatchId as string);
|
|
50
|
+
|
|
51
|
+
const selector = useStructuralSharing(opts, router, subSlot(slot, 'm'));
|
|
52
|
+
const matchSelection = useStore(
|
|
53
|
+
matchStore ?? dummyStore,
|
|
54
|
+
(match: any) => (match ? selector(match) : dummyStore),
|
|
55
|
+
undefined,
|
|
56
|
+
subSlot(slot, 'm:us'),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (matchSelection !== dummyStore) return matchSelection;
|
|
60
|
+
if (opts.shouldThrow ?? true) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Invariant failed: Could not find ${
|
|
63
|
+
opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'
|
|
64
|
+
}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function useParams(...args: any[]): any {
|
|
71
|
+
const [user, slot] = splitSlot(args);
|
|
72
|
+
const opts = user[0] ?? {};
|
|
73
|
+
return useMatch(
|
|
74
|
+
{
|
|
75
|
+
from: opts.from,
|
|
76
|
+
strict: opts.strict,
|
|
77
|
+
shouldThrow: opts.shouldThrow,
|
|
78
|
+
structuralSharing: opts.structuralSharing,
|
|
79
|
+
select: (match: any) => {
|
|
80
|
+
const params = opts.strict === false ? match.params : match._strictParams;
|
|
81
|
+
return opts.select ? opts.select(params) : params;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
subSlot(slot, 'params'),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function useSearch(...args: any[]): any {
|
|
89
|
+
const [user, slot] = splitSlot(args);
|
|
90
|
+
const opts = user[0] ?? {};
|
|
91
|
+
return useMatch(
|
|
92
|
+
{
|
|
93
|
+
from: opts.from,
|
|
94
|
+
strict: opts.strict,
|
|
95
|
+
shouldThrow: opts.shouldThrow,
|
|
96
|
+
structuralSharing: opts.structuralSharing,
|
|
97
|
+
select: (match: any) => (opts.select ? opts.select(match.search) : match.search),
|
|
98
|
+
},
|
|
99
|
+
subSlot(slot, 'search'),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function useLoaderData(...args: any[]): any {
|
|
104
|
+
const [user, slot] = splitSlot(args);
|
|
105
|
+
const opts = user[0] ?? {};
|
|
106
|
+
return useMatch(
|
|
107
|
+
{
|
|
108
|
+
from: opts.from,
|
|
109
|
+
strict: opts.strict,
|
|
110
|
+
structuralSharing: opts.structuralSharing,
|
|
111
|
+
select: (match: any) => (opts.select ? opts.select(match.loaderData) : match.loaderData),
|
|
112
|
+
},
|
|
113
|
+
subSlot(slot, 'loader'),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function useLoaderDeps(...args: any[]): any {
|
|
118
|
+
const [user, slot] = splitSlot(args);
|
|
119
|
+
const opts = user[0] ?? {};
|
|
120
|
+
const { select, ...rest } = opts;
|
|
121
|
+
return useMatch(
|
|
122
|
+
{
|
|
123
|
+
...rest,
|
|
124
|
+
select: (match: any) => (select ? select(match.loaderDeps) : match.loaderDeps),
|
|
125
|
+
},
|
|
126
|
+
subSlot(slot, 'deps'),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function useRouteContext(...args: any[]): any {
|
|
131
|
+
const [user, slot] = splitSlot(args);
|
|
132
|
+
const opts = user[0] ?? {};
|
|
133
|
+
return useMatch(
|
|
134
|
+
{
|
|
135
|
+
...opts,
|
|
136
|
+
select: (match: any) => (opts.select ? opts.select(match.context) : match.context),
|
|
137
|
+
},
|
|
138
|
+
subSlot(slot, 'ctx'),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function useLocation(...args: any[]): any {
|
|
143
|
+
const [user, slot] = splitSlot(args);
|
|
144
|
+
const opts = user[0] ?? {};
|
|
145
|
+
const router = useRouter();
|
|
146
|
+
return useStore(
|
|
147
|
+
router.stores.location,
|
|
148
|
+
useStructuralSharing(opts, router, subSlot(slot, 'loc')),
|
|
149
|
+
undefined,
|
|
150
|
+
subSlot(slot, 'loc:us'),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function useMatches(...args: any[]): any {
|
|
155
|
+
const [user, slot] = splitSlot(args);
|
|
156
|
+
const opts = user[0] ?? {};
|
|
157
|
+
const router = useRouter();
|
|
158
|
+
return useStore(
|
|
159
|
+
router.stores.matches,
|
|
160
|
+
useStructuralSharing(opts, router, subSlot(slot, 'matches')),
|
|
161
|
+
undefined,
|
|
162
|
+
subSlot(slot, 'matches:us'),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function useParentMatches(...args: any[]): any {
|
|
167
|
+
const [user, slot] = splitSlot(args);
|
|
168
|
+
const opts = user[0] ?? {};
|
|
169
|
+
const contextMatchId = useContext(matchContext);
|
|
170
|
+
return useMatches(
|
|
171
|
+
{
|
|
172
|
+
select: (matches: any[]) => {
|
|
173
|
+
matches = matches.slice(
|
|
174
|
+
0,
|
|
175
|
+
matches.findIndex((d: any) => d.id === contextMatchId),
|
|
176
|
+
);
|
|
177
|
+
return opts.select ? opts.select(matches) : matches;
|
|
178
|
+
},
|
|
179
|
+
structuralSharing: opts.structuralSharing,
|
|
180
|
+
},
|
|
181
|
+
subSlot(slot, 'parents'),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function useChildMatches(...args: any[]): any {
|
|
186
|
+
const [user, slot] = splitSlot(args);
|
|
187
|
+
const opts = user[0] ?? {};
|
|
188
|
+
const contextMatchId = useContext(matchContext);
|
|
189
|
+
return useMatches(
|
|
190
|
+
{
|
|
191
|
+
select: (matches: any[]) => {
|
|
192
|
+
matches = matches.slice(matches.findIndex((d: any) => d.id === contextMatchId) + 1);
|
|
193
|
+
return opts.select ? opts.select(matches) : matches;
|
|
194
|
+
},
|
|
195
|
+
structuralSharing: opts.structuralSharing,
|
|
196
|
+
},
|
|
197
|
+
subSlot(slot, 'children'),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Returns a STABLE navigate function (upstream useCallback([from, router])) that
|
|
202
|
+
// forwards to `router.navigate`, defaulting `from` to the hook's option.
|
|
203
|
+
export function useNavigate(...args: any[]): (to: any) => any {
|
|
204
|
+
const [user, slot] = splitSlot(args);
|
|
205
|
+
const opts = user[0] ?? {};
|
|
206
|
+
const router = useRouter(opts.router ? { router: opts.router } : undefined);
|
|
207
|
+
return useCallback(
|
|
208
|
+
(options: any) => router.navigate({ ...options, from: options?.from ?? opts.from }),
|
|
209
|
+
[opts.from, router],
|
|
210
|
+
subSlot(slot, 'nav'),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// True when the current history entry isn't the first (there is somewhere to go
|
|
215
|
+
// back to) — per upstream useCanGoBack (location.state.__TSR_index !== 0).
|
|
216
|
+
export function useCanGoBack(...args: any[]): boolean {
|
|
217
|
+
const [, slot] = splitSlot(args);
|
|
218
|
+
const router = useRouter();
|
|
219
|
+
return useStore(
|
|
220
|
+
router.stores.location,
|
|
221
|
+
(location: any) => location.state.__TSR_index !== 0,
|
|
222
|
+
undefined,
|
|
223
|
+
subSlot(slot, 'back'),
|
|
224
|
+
);
|
|
225
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @octanejs/tanstack-router — TanStack Router for the octane renderer.
|
|
2
|
+
//
|
|
3
|
+
// TanStack Router splits a framework-agnostic core (`@tanstack/router-core`: the
|
|
4
|
+
// Router/route-tree/matching/history/reactive-store) from a thin React binding
|
|
5
|
+
// (`@tanstack/react-router`). Mirroring @octanejs/tanstack-query, this package re-exports
|
|
6
|
+
// the core VERBATIM and reimplements only the React binding on octane's hooks. The
|
|
7
|
+
// load-bearing seam is router-core's reactive store: `createRouter` supplies the
|
|
8
|
+
// CLIENT store factory (`createAtom`/`batch` from `@tanstack/store`), whose atoms
|
|
9
|
+
// expose `.subscribe`/`.get` — bound to octane's `useSyncExternalStore` by
|
|
10
|
+
// `useStore`. The match tree renders pull-based: `RouterProvider` → first match →
|
|
11
|
+
// each route's `<Outlet/>` looks up the NEXT match via `matchContext`.
|
|
12
|
+
//
|
|
13
|
+
// Scope: code-based routing at react-router parity — RouterProvider (+
|
|
14
|
+
// RouterContextProvider/Wrap/InnerWrap), the full Match pipeline (Suspense /
|
|
15
|
+
// CatchBoundary / CatchNotFound per route, pending/error/redirect statuses,
|
|
16
|
+
// remountDeps, shellComponent), the router event lifecycle
|
|
17
|
+
// (onLoad/onBeforeRouteMount/onResolved/onRendered + resolvedLocation — scroll
|
|
18
|
+
// restoration restores off it), Link with preloading/masking/active-options,
|
|
19
|
+
// createLink/useLinkProps, navigation blocking (useBlocker/Block), the full
|
|
20
|
+
// read-hook set (useMatch and friends, nearest-match resolution via
|
|
21
|
+
// matchContext), Route/getRouteApi hook accessors, Await/useAwaited, lazy
|
|
22
|
+
// routes, and search validation/middleware from core. Deferred: file-based
|
|
23
|
+
// routing + codegen (`createFileRoute`, @tanstack/router-plugin), SSR entries
|
|
24
|
+
// (RouterServer/RouterClient, HeadContent/Scripts), and devtools.
|
|
25
|
+
export * from '@tanstack/router-core';
|
|
26
|
+
export {
|
|
27
|
+
createHistory,
|
|
28
|
+
createBrowserHistory,
|
|
29
|
+
createHashHistory,
|
|
30
|
+
createMemoryHistory,
|
|
31
|
+
} from './history';
|
|
32
|
+
// History types on the main entry (upstream parity). `NavigateOptions` is NOT
|
|
33
|
+
// re-exported from history — router-core's richer NavigateOptions wins.
|
|
34
|
+
export type {
|
|
35
|
+
RouterHistory,
|
|
36
|
+
HistoryLocation,
|
|
37
|
+
ParsedPath,
|
|
38
|
+
HistoryState,
|
|
39
|
+
ParsedHistoryState,
|
|
40
|
+
HistoryAction,
|
|
41
|
+
BlockerFnArgs,
|
|
42
|
+
BlockerFn,
|
|
43
|
+
NavigationBlocker,
|
|
44
|
+
} from '@tanstack/history';
|
|
45
|
+
|
|
46
|
+
export { createRouter, Router } from './router';
|
|
47
|
+
export {
|
|
48
|
+
createRoute,
|
|
49
|
+
createRootRoute,
|
|
50
|
+
createRootRouteWithContext,
|
|
51
|
+
createRouteMask,
|
|
52
|
+
getRouteApi,
|
|
53
|
+
Route,
|
|
54
|
+
RootRoute,
|
|
55
|
+
RouteApi,
|
|
56
|
+
} from './route';
|
|
57
|
+
export { routerContext, getRouterContext, matchContext, useRouter } from './context';
|
|
58
|
+
export { useStore } from './useStore';
|
|
59
|
+
export { useRouterState } from './useRouterState';
|
|
60
|
+
export {
|
|
61
|
+
useMatch,
|
|
62
|
+
useLocation,
|
|
63
|
+
useParams,
|
|
64
|
+
useSearch,
|
|
65
|
+
useLoaderData,
|
|
66
|
+
useLoaderDeps,
|
|
67
|
+
useRouteContext,
|
|
68
|
+
useMatches,
|
|
69
|
+
useParentMatches,
|
|
70
|
+
useChildMatches,
|
|
71
|
+
useNavigate,
|
|
72
|
+
useCanGoBack,
|
|
73
|
+
} from './hooks';
|
|
74
|
+
export { useAwaited } from './useAwaited';
|
|
75
|
+
export { useLinkProps, createLink, linkOptions } from './link';
|
|
76
|
+
export { useBlocker, Block } from './useBlocker.tsrx';
|
|
77
|
+
export { useMatchRoute, MatchRoute } from './MatchRoute.tsrx';
|
|
78
|
+
export { useElementScrollRestoration } from './useElementScrollRestoration';
|
|
79
|
+
export { lazyRouteComponent } from './lazyRouteComponent';
|
|
80
|
+
|
|
81
|
+
export { RouterProvider, RouterContextProvider } from './RouterProvider.tsrx';
|
|
82
|
+
export { Outlet } from './Outlet.tsrx';
|
|
83
|
+
export { Link } from './Link.tsrx';
|
|
84
|
+
export { Navigate } from './Navigate.tsrx';
|
|
85
|
+
export { Await } from './Await.tsrx';
|
|
86
|
+
export { ScrollRestoration } from './ScrollRestoration.tsrx';
|
|
87
|
+
export { Matches } from './Matches.tsrx';
|
|
88
|
+
export { Match } from './Match.tsrx';
|
|
89
|
+
export { CatchBoundary, ErrorComponent } from './CatchBoundary.tsrx';
|
|
90
|
+
export { CatchNotFound, DefaultGlobalNotFound } from './not-found.tsrx';
|
|
91
|
+
export { ClientOnly, useHydrated } from './ClientOnly.tsrx';
|