@native-router/react 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -12
- package/dist/types/create-routes.d.ts +22 -7
- package/dist/types/types.d.ts +71 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -74,7 +74,8 @@ function Preview({visible}: {visible: boolean}) {
|
|
|
74
74
|
- `NavLink` with `isActive`/`isExactActive`, `end`, `caseSensitive` and `aria-current` (defaults to `"page"`); `className`/`style`/`children` accept `({isActive, isExactActive})` callbacks; `to="/"` is active for every path
|
|
75
75
|
- Polymorphic links: every link component takes an `as` component — own props flattened and type-checked on the link, colliding props through the `asProps` escape hatch, `href`/`onClick`/`aria-current` injected, `ref` forwarded
|
|
76
76
|
- `useSearchParams` reads and writes the query string; writes push by default or replace with `{replace: true}`; `useSetSearch(schema)` is the schema-aware setter twin of `useSearch(schema)` — the next value is validated by the same schema before any navigation, a rejection throws `SearchError` without touching the location, and the written query is the schema's own output(defaults applied)
|
|
77
|
-
- Typed search: an optional Standard Schema validator (zod/valibot/arktype, no hard dependency) on any route `search` field, parsed at resolve time — loaders receive a typed `ctx.search` and an invalid search fails the level through the existing error layers; `useSearch(schema?)` reads it in components, degrading to the raw object without a schema
|
|
77
|
+
- Typed search: an optional Standard Schema validator (zod/valibot/arktype, no hard dependency) on any route `search` field, parsed at resolve time — `data` loaders and `beforeLoad` guards receive a typed `ctx.search` and an invalid search fails the level through the existing error layers; `useSearch(schema?)` reads it in components, degrading to the raw object without a schema
|
|
78
|
+
- Search type closure: `createRoutes(routes)` re-types the returned table so every level's `data`/`beforeLoad` `ctx.search` derives from the level's own schema — no `Route<P, S>` generics or callback annotations needed; an explicit `Route<P, S>` generic still wins wherever written
|
|
78
79
|
- Type-safe links: `createRoutes(routes)` checks the table while keeping every `path` literal, `RoutePaths<typeof routes>` extracts the pattern union(through nesting and param segments), and `<TypedLink<RoutePaths<...>> to params>` narrows `to` to the table and checks `params` against the exact pattern's segments — compile errors for unknown paths and missing/wrong params, click-time interpolation with encoding as the runtime backstop
|
|
79
80
|
- `ScrollRestoration` restores the scroll offset per history entry on back/forward and resets it on push (`resetOnPush` to opt out)
|
|
80
81
|
- Router-level `preload(router, to)` shares resolved views across links with in-flight dedup and a 30s TTL; `PrefetchLink` prefetch through it
|
|
@@ -259,11 +260,12 @@ import {ScrollRestoration} from '@native-router/react';
|
|
|
259
260
|
|
|
260
261
|
On mount it also sets `history.scrollRestoration` to `manual`: the browser's own `auto` restoration would race the component's restore and pre-scroll while the left entry's offset is still being read, so the component owns scroll restoration for the session (the setting is not reverted on unmount).
|
|
261
262
|
|
|
262
|
-
Validate and type the search with a schema — any zod/valibot/arktype schema works, the router only speaks [Standard Schema](https://standardschema.dev). Declare it once on the route and the search is parsed during resolve: the `data` loader
|
|
263
|
+
Validate and type the search with a schema — any zod/valibot/arktype schema works, the router only speaks [Standard Schema](https://standardschema.dev). Declare it once on the route and the search is parsed during resolve: the `data` loader and the `beforeLoad` guard receive a typed `ctx.search` (coerced numbers, defaults applied), and an invalid search fails the level through the existing error layers — the route `errorComponent`, else the global `errorHandler`.
|
|
264
|
+
|
|
265
|
+
Build the table with `createRoutes` and the typing closes by itself: the returned table derives every level's `ctx.search` from the level's own schema, so neither the manual `Route<P, S>` generic nor callback annotations are needed. (Callbacks written inside the literal are checked loosely — `ctx.search: any` — since TypeScript cannot contextually type a member from sibling properties; the precise types hold on the returned table, and a callback annotation that contradicts the schema is rejected at the property.)
|
|
263
266
|
|
|
264
267
|
```tsx
|
|
265
|
-
import {useData, useSearch} from '@native-router/react';
|
|
266
|
-
import type {Route} from '@native-router/react';
|
|
268
|
+
import {createRoutes, useData, useSearch} from '@native-router/react';
|
|
267
269
|
import {z} from 'zod';
|
|
268
270
|
|
|
269
271
|
const listSearch = z.object({
|
|
@@ -271,14 +273,19 @@ const listSearch = z.object({
|
|
|
271
273
|
tag: z.string().optional()
|
|
272
274
|
});
|
|
273
275
|
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
276
|
+
const routes = createRoutes({
|
|
277
|
+
component: () => import('./Layout'),
|
|
278
|
+
children: [
|
|
279
|
+
{
|
|
280
|
+
path: '/articles',
|
|
281
|
+
search: listSearch,
|
|
282
|
+
component: () => import('./ArticleList'),
|
|
283
|
+
// typeof routes → this level's ctx.search: {page: number; tag?: string}
|
|
284
|
+
data: ({search}) => fetchArticles(search.page, search.tag),
|
|
285
|
+
errorComponent: ({error}) => <p>{error.message}</p>
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
});
|
|
282
289
|
|
|
283
290
|
function ArticleList() {
|
|
284
291
|
const articles = useData<Article[]>(); // typed, no casts
|
|
@@ -288,6 +295,16 @@ function ArticleList() {
|
|
|
288
295
|
}
|
|
289
296
|
```
|
|
290
297
|
|
|
298
|
+
Hand-annotated route objects keep the explicit generic — it wins wherever written:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
const listRoute = {
|
|
302
|
+
path: '/articles',
|
|
303
|
+
search: listSearch,
|
|
304
|
+
component: () => import('./ArticleList')
|
|
305
|
+
} as Route<'/articles', {page: number; tag?: string}>;
|
|
306
|
+
```
|
|
307
|
+
|
|
291
308
|
`useSearch()` without a schema degrades to the raw input object of `parseSearchInput` (strings; repeated keys are arrays) and needs no schema on the route. Both flavors re-render on every location change, and the schema must validate synchronously.
|
|
292
309
|
|
|
293
310
|
Write the search through the same schema — `useSetSearch(schema)` validates the next value before any navigation, throws `SearchError` (with the schema's issues) without touching the location when it rejects, and writes the schema's own output so defaults apply:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Route } from './types';
|
|
1
|
+
import type { Route, SearchRoutesOf } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Identity function with `satisfies` semantics: the table is checked
|
|
4
4
|
* against `Route` while every `path` keeps its string-literal type, so
|
|
@@ -6,22 +6,37 @@ import type { Route } from './types';
|
|
|
6
6
|
* `TypedLink`. An `as Route` assertion does the opposite — it widens
|
|
7
7
|
* every `path` to `string` and gives up the literals.
|
|
8
8
|
*
|
|
9
|
+
* The return type additionally closes the search loop(see
|
|
10
|
+
* {@link SearchRoutesOf}): every level's `data` loader and `beforeLoad`
|
|
11
|
+
* guard receive their `ctx.search` typed from the level's own
|
|
12
|
+
* {@link Route.search search schema} —
|
|
13
|
+
*
|
|
9
14
|
* ```tsx
|
|
10
15
|
* const routes = createRoutes({
|
|
11
16
|
* children: [
|
|
12
|
-
* {
|
|
13
|
-
*
|
|
17
|
+
* {
|
|
18
|
+
* path: '/list',
|
|
19
|
+
* search: z.object({page: z.coerce.number()}),
|
|
20
|
+
* // typeof routes → ctx.search: {page: number}, no annotations
|
|
21
|
+
* data: ({search}) => fetchList(search.page)
|
|
22
|
+
* }
|
|
14
23
|
* ]
|
|
15
24
|
* });
|
|
16
|
-
* // type AppPaths = '/' | '/users/:id'
|
|
17
|
-
* type AppPaths = RoutePaths<typeof routes>;
|
|
18
25
|
* ```
|
|
19
26
|
*
|
|
27
|
+
* Callbacks written inside the literal are still checked loosely
|
|
28
|
+
* against `Route`(`ctx.search: any` — TypeScript cannot contextually
|
|
29
|
+
* type a member from sibling properties); the precise types hold on the
|
|
30
|
+
* returned table, and a callback whose annotation contradicts the
|
|
31
|
+
* schema is rejected at the property. An explicit `Route<P, S>` generic
|
|
32
|
+
* keeps priority wherever it is written.
|
|
33
|
+
*
|
|
20
34
|
* Zero runtime cost: the function returns its argument unchanged and
|
|
21
35
|
* tree-shakes away.
|
|
22
36
|
* @group Methods
|
|
23
37
|
* @category Route
|
|
24
38
|
* @param routes the route table, a route object or an array of them
|
|
25
|
-
* @returns the very same route table, literal types preserved
|
|
39
|
+
* @returns the very same route table, literal types preserved and the
|
|
40
|
+
* loader/guard search contexts re-typed from the schemas
|
|
26
41
|
*/
|
|
27
|
-
export declare function createRoutes<const T>(routes: T & (Route | Route[])): T
|
|
42
|
+
export declare function createRoutes<const T>(routes: T & SearchRoutesOf<T> & (Route | Route[])): SearchRoutesOf<T>;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnchorHTMLAttributes, ComponentPropsWithRef, ComponentPropsWithoutRef, ComponentType, CSSProperties, DetailedHTMLProps, ElementType, ReactNode } from 'react';
|
|
2
|
-
import type { BaseRoute, ExtractPathParams, Matched, Location, RouterInstance, SearchInput } from '@native-router/core';
|
|
2
|
+
import type { Awaitable, BaseRoute, ExtractPathParams, GuardContext, Matched, Location, RouterInstance, SearchInput, StandardSchemaV1 } from '@native-router/core';
|
|
3
3
|
export type ResolveViewContext<R extends BaseRoute> = {
|
|
4
4
|
router: RouterInstance<R>;
|
|
5
5
|
location: Location;
|
|
@@ -55,14 +55,18 @@ export type RouteParams<P extends string> = string extends P ? Record<string, st
|
|
|
55
55
|
* `Route<'/users/:id'>` → `params: {id: string}`. Give the second
|
|
56
56
|
* generic the output type of the route `search` schema and `ctx.search`
|
|
57
57
|
* is typed accordingly, e.g. `Route<'/list', {page: number}>` with
|
|
58
|
-
* `search: z.object({page: z.coerce.number()})` → `search: {page: number}
|
|
58
|
+
* `search: z.object({page: z.coerce.number()})` → `search: {page: number}` —
|
|
59
|
+
* for the `data` loader and the `beforeLoad` guard alike.
|
|
59
60
|
*
|
|
60
61
|
* Without the search generic an untyped `ctx.search` stays `any` — the
|
|
61
62
|
* default that keeps differently typed levels assignable to plain
|
|
62
63
|
* `Route`: schema outputs are arbitrary(coerced numbers, defaults, ...),
|
|
63
64
|
* so no single degraded shape is bivariant with all of them. At runtime
|
|
64
65
|
* it holds the raw input object of `parseSearchInput`; see `useSearch`
|
|
65
|
-
* for the typed degraded shape.
|
|
66
|
+
* for the typed degraded shape. Prefer {@link createRoutes}: its
|
|
67
|
+
* returned table derives every level's `ctx.search` from the level's own
|
|
68
|
+
* schema(see {@link SearchRoutesOf}), so the manual generic is only
|
|
69
|
+
* needed for hand-annotated route objects.
|
|
66
70
|
* `children` accepts `Route<any, any>` so levels with different patterns
|
|
67
71
|
* and search shapes nest without variance conflicts.
|
|
68
72
|
* @group Types
|
|
@@ -99,7 +103,17 @@ export type Route<P extends string = string, S = any> = Omit<BaseRoute<{
|
|
|
99
103
|
* Receives no props.
|
|
100
104
|
*/
|
|
101
105
|
pendingComponent?: ComponentType;
|
|
102
|
-
}>, 'path' | 'children'> & {
|
|
106
|
+
}>, 'path' | 'children' | 'beforeLoad'> & {
|
|
107
|
+
/**
|
|
108
|
+
* Route guard inherited from `BaseRoute`, re-typed by the search
|
|
109
|
+
* generic: `ctx.search` is `S`(`any` by default — see the Route doc
|
|
110
|
+
* above). At runtime it holds the level's parsed search — the schema
|
|
111
|
+
* output, or the degraded input without a schema. The guard context
|
|
112
|
+
* types `router` as `RouterInstance<any>`: a precise
|
|
113
|
+
* `RouterInstance<Route>` here would recurse into `Route`'s own
|
|
114
|
+
* members and break `Route`'s assignability to plain `BaseRoute`.
|
|
115
|
+
*/
|
|
116
|
+
beforeLoad?(ctx: GuardContext<any, S>): Awaitable<string | void>;
|
|
103
117
|
/** Path pattern; params of the contexts above are inferred from it. */
|
|
104
118
|
path?: P;
|
|
105
119
|
/**
|
|
@@ -114,6 +128,59 @@ export type LoadStatus = {
|
|
|
114
128
|
key: number;
|
|
115
129
|
status: 'pending' | 'resolved' | 'rejected';
|
|
116
130
|
};
|
|
131
|
+
/**
|
|
132
|
+
* The search a route's `data`/`beforeLoad` contexts receive: the
|
|
133
|
+
* level's own {@link Route.search search schema} output, or the
|
|
134
|
+
* degraded {@link SearchInput} when the level declares no schema.
|
|
135
|
+
* Building block of {@link SearchRoutesOf}.
|
|
136
|
+
* @group Types
|
|
137
|
+
* @category Route
|
|
138
|
+
*/
|
|
139
|
+
export type RouteSearchOf<R> = R extends {
|
|
140
|
+
search: StandardSchemaV1<any, infer Output>;
|
|
141
|
+
} ? Output : SearchInput;
|
|
142
|
+
/**
|
|
143
|
+
* A context with only its `search` member replaced — everything the
|
|
144
|
+
* callback declared(`params` precision, `signal`, custom shapes)
|
|
145
|
+
* passes through untouched.
|
|
146
|
+
* @group Types
|
|
147
|
+
* @category Route
|
|
148
|
+
*/
|
|
149
|
+
export type WithSearch<C, S> = Omit<C, 'search'> & {
|
|
150
|
+
search: S;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Re-type a route table so every level's `data` loader and `beforeLoad`
|
|
154
|
+
* guard derive their `ctx.search` from the level's own
|
|
155
|
+
* {@link Route.search search schema}: the schema's parsed output(see
|
|
156
|
+
* {@link RouteSearchOf}) instead of the loose `any`. This is what
|
|
157
|
+
* {@link createRoutes} returns, closing the type loop — no manual
|
|
158
|
+
* `Route<P, S>` generics or callback annotations needed for the search
|
|
159
|
+
* typing.
|
|
160
|
+
*
|
|
161
|
+
* Everything else passes through unchanged: `path` literals(so
|
|
162
|
+
* `RoutePaths<typeof routes>` and `TypedLink` keep working), the
|
|
163
|
+
* loaders' return types, and the rest of every level's members. A
|
|
164
|
+
* callback that annotates its ctx with a search shape the schema
|
|
165
|
+
* contradicts is rejected at the `data`/`beforeLoad` property; an
|
|
166
|
+
* un-annotated callback written inside the literal is checked loosely
|
|
167
|
+
* against `Route`(`ctx.search: any` — TypeScript cannot contextually
|
|
168
|
+
* type a member from sibling properties) and precisely on the returned
|
|
169
|
+
* table.
|
|
170
|
+
* @group Types
|
|
171
|
+
* @category Route
|
|
172
|
+
*/
|
|
173
|
+
export type SearchRoutesOf<T> = T extends readonly (infer _Level)[] ? {
|
|
174
|
+
-readonly [K in keyof T]: SearchRoutesOf<T[K]>;
|
|
175
|
+
} : T extends Route ? T extends {
|
|
176
|
+
data?: infer Data;
|
|
177
|
+
beforeLoad?: infer BeforeLoad;
|
|
178
|
+
children?: infer Children;
|
|
179
|
+
} ? Omit<T, 'data' | 'beforeLoad' | 'children'> & {
|
|
180
|
+
data?: [unknown] extends [Data] ? undefined : Data extends (ctx: infer DataCtx) => infer R ? (ctx: WithSearch<DataCtx, RouteSearchOf<T>>) => R : Data;
|
|
181
|
+
beforeLoad?: [unknown] extends [BeforeLoad] ? undefined : BeforeLoad extends (ctx: infer GuardCtx) => infer R ? (ctx: WithSearch<GuardCtx, RouteSearchOf<T>>) => R : BeforeLoad;
|
|
182
|
+
children?: [unknown] extends [Children] ? undefined : SearchRoutesOf<Children>;
|
|
183
|
+
} : T : T;
|
|
117
184
|
/**
|
|
118
185
|
* Union of every navigable path pattern of a route table, computed from
|
|
119
186
|
* the table's type. Each level's `path` literal concatenates with its
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@native-router/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@native-router/core": "^1.
|
|
65
|
+
"@native-router/core": "^1.8.0",
|
|
66
66
|
"history": "^5.3.0",
|
|
67
67
|
"use-sync-external-store": "^1.6.0"
|
|
68
68
|
},
|