@ovineko/react-router 0.0.2 → 0.0.3
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 +95 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -16
- package/dist/utils.d.ts +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,20 +16,22 @@ Peer dependencies: `react@^19`, `react-router@^7`, `valibot@^1`.
|
|
|
16
16
|
- ✅ **Two hook variants** - Normal hooks (auto-redirect on error) and Raw hooks (manual error handling)
|
|
17
17
|
- ✅ **Smart validation** - Ignores extra query params and invalid optional params
|
|
18
18
|
- ✅ **Global error handling** - Configure fallback redirect URLs globally or per-route
|
|
19
|
+
- ✅ **Conditional redirects** - `useRedirect` hook for declarative navigation with infinite loop prevention
|
|
20
|
+
- ✅ **Optional search params helper** - `optionalSearchParams` utility to avoid repetitive `v.optional()` calls
|
|
19
21
|
- ✅ **Type-safe** - Full TypeScript support with inferred types
|
|
20
22
|
- ✅ **Hash support** - Generate paths with hash fragments
|
|
21
23
|
|
|
22
24
|
## Quick Start
|
|
23
25
|
|
|
24
26
|
```tsx
|
|
25
|
-
import { createRouteWithParams } from "@ovineko/react-router";
|
|
27
|
+
import { createRouteWithParams, optionalSearchParams } from "@ovineko/react-router";
|
|
26
28
|
import * as v from "valibot";
|
|
27
29
|
|
|
28
30
|
const userRoute = createRouteWithParams("/users/:id", {
|
|
29
31
|
params: v.object({ id: v.pipe(v.string(), v.uuid()) }),
|
|
30
|
-
searchParams:
|
|
31
|
-
tab: v.
|
|
32
|
-
page: v.
|
|
32
|
+
searchParams: optionalSearchParams({
|
|
33
|
+
tab: v.string(),
|
|
34
|
+
page: v.pipe(v.string(), v.transform(Number), v.number()),
|
|
33
35
|
}),
|
|
34
36
|
errorRedirect: "/404", // Optional: redirect on validation error
|
|
35
37
|
});
|
|
@@ -207,6 +209,95 @@ setSearchParams({ q: "vue" }, { replace: true });
|
|
|
207
209
|
|
|
208
210
|
### Utilities
|
|
209
211
|
|
|
212
|
+
#### `useRedirect(path, condition, replace?)`
|
|
213
|
+
|
|
214
|
+
Declarative hook for conditional redirects with built-in infinite loop prevention.
|
|
215
|
+
|
|
216
|
+
**Parameters:**
|
|
217
|
+
|
|
218
|
+
- `path: string` - Target redirect URL
|
|
219
|
+
- `condition: boolean` - Whether to trigger the redirect
|
|
220
|
+
- `replace?: boolean` - Use replace instead of push (default: `true`)
|
|
221
|
+
|
|
222
|
+
**Features:**
|
|
223
|
+
|
|
224
|
+
- Prevents infinite redirect loops using `useRef` tracking
|
|
225
|
+
- Only redirects once when condition becomes `true`
|
|
226
|
+
- Resets automatically when condition becomes `false`
|
|
227
|
+
- Uses `replace: true` by default to prevent back-button issues
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import { useRedirect } from "@ovineko/react-router";
|
|
231
|
+
|
|
232
|
+
function ProtectedPage() {
|
|
233
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
234
|
+
|
|
235
|
+
// Redirect to login if not authenticated
|
|
236
|
+
useRedirect("/login", !isAuthenticated && !isLoading);
|
|
237
|
+
|
|
238
|
+
if (isLoading) return <Spinner />;
|
|
239
|
+
return <div>Protected Content</div>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Advanced usage with custom options
|
|
243
|
+
function UserProfile() {
|
|
244
|
+
const { user, error } = useUser();
|
|
245
|
+
|
|
246
|
+
// Redirect without replacing history
|
|
247
|
+
useRedirect("/users", !user && !error, false);
|
|
248
|
+
|
|
249
|
+
return <div>{user?.name}</div>;
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### `optionalSearchParams(entries)`
|
|
254
|
+
|
|
255
|
+
Utility to make all search param fields optional automatically, avoiding repetitive `v.optional()` calls.
|
|
256
|
+
|
|
257
|
+
**Parameters:**
|
|
258
|
+
|
|
259
|
+
- `entries: ObjectEntries` - Valibot schema entries for search params
|
|
260
|
+
|
|
261
|
+
**Returns:** Valibot object schema with all fields wrapped in `v.optional()`
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
import { optionalSearchParams } from "@ovineko/react-router";
|
|
265
|
+
import * as v from "valibot";
|
|
266
|
+
|
|
267
|
+
// ❌ Before: Manual v.optional() for each field
|
|
268
|
+
const route = createRouteWithParams("/search", {
|
|
269
|
+
params: v.object({ id: v.string() }),
|
|
270
|
+
searchParams: v.object({
|
|
271
|
+
q: v.optional(v.string()),
|
|
272
|
+
page: v.optional(v.pipe(v.string(), v.transform(Number), v.number())),
|
|
273
|
+
sort: v.optional(v.string()),
|
|
274
|
+
filter: v.optional(v.string()),
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ✅ After: Clean and concise
|
|
279
|
+
const route = createRouteWithParams("/search", {
|
|
280
|
+
params: v.object({ id: v.string() }),
|
|
281
|
+
searchParams: optionalSearchParams({
|
|
282
|
+
q: v.string(),
|
|
283
|
+
page: v.pipe(v.string(), v.transform(Number), v.number()),
|
|
284
|
+
sort: v.string(),
|
|
285
|
+
filter: v.string(),
|
|
286
|
+
}),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// All fields are automatically optional!
|
|
290
|
+
const [searchParams] = route.useSearchParams();
|
|
291
|
+
// Type: Readonly<{ q?: string; page?: number; sort?: string; filter?: string }>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Benefits:**
|
|
295
|
+
|
|
296
|
+
- Cleaner, more readable code
|
|
297
|
+
- Less boilerplate for search params (which are typically optional)
|
|
298
|
+
- Full TypeScript support with proper type inference
|
|
299
|
+
- Works with any Valibot schema (transformations, pipes, etc.)
|
|
300
|
+
|
|
210
301
|
#### `setGlobalErrorRedirect(url)`
|
|
211
302
|
|
|
212
303
|
Set global fallback redirect URL for validation errors.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import type { InferSearchParams, RouteWithoutParams, RouteWithParams, State } from "./types";
|
|
3
3
|
export type { RouteBase, RouteWithoutParams, RouteWithParams, State, UseParamsRawResult, UseSearchParamsRawResult, } from "./types";
|
|
4
|
+
export { optionalSearchParams } from "./utils";
|
|
4
5
|
export { URLParseError } from "./validation";
|
|
5
6
|
export declare const setGlobalErrorRedirect: (url: string) => void;
|
|
7
|
+
export declare const useRedirect: (path: string, is: boolean, replace?: boolean) => void;
|
|
6
8
|
export declare const createRouteWithParams: <TParams extends v.GenericSchema, TSearchParams extends undefined | v.GenericSchema = undefined>(pattern: string, config: {
|
|
7
9
|
errorRedirect?: string;
|
|
8
10
|
params: TParams;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.tsx
|
|
2
|
-
import { memo, useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import {
|
|
4
4
|
generatePath,
|
|
5
5
|
Link,
|
|
@@ -141,6 +141,15 @@ var cleanOptionalParams = (obj, schema) => {
|
|
|
141
141
|
}
|
|
142
142
|
return cleaned;
|
|
143
143
|
};
|
|
144
|
+
var optionalSearchParams = (entries) => {
|
|
145
|
+
const optionalEntries = {};
|
|
146
|
+
for (const key in entries) {
|
|
147
|
+
if (Object.hasOwn(entries, key)) {
|
|
148
|
+
optionalEntries[key] = v.optional(entries[key]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return v.object(optionalEntries);
|
|
152
|
+
};
|
|
144
153
|
|
|
145
154
|
// src/index.tsx
|
|
146
155
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -151,6 +160,18 @@ var setGlobalErrorRedirect = (url) => {
|
|
|
151
160
|
var getErrorRedirectUrl = (routeLevel) => {
|
|
152
161
|
return routeLevel ?? globalErrorRedirect;
|
|
153
162
|
};
|
|
163
|
+
var useRedirect = (path, is, replace = true) => {
|
|
164
|
+
const navigate = useNavigate();
|
|
165
|
+
const hasRedirectedRef = useRef(false);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (is && !hasRedirectedRef.current) {
|
|
168
|
+
hasRedirectedRef.current = true;
|
|
169
|
+
navigate(path, { replace });
|
|
170
|
+
} else if (!is) {
|
|
171
|
+
hasRedirectedRef.current = false;
|
|
172
|
+
}
|
|
173
|
+
}, [is, navigate, path, replace]);
|
|
174
|
+
};
|
|
154
175
|
var createSearchParamsHook = (schema) => {
|
|
155
176
|
const useSearchParamsRaw = () => {
|
|
156
177
|
const [rawSearchParams, setRawSearchParams] = useSearchParams();
|
|
@@ -194,14 +215,9 @@ var createSearchParamsHook = (schema) => {
|
|
|
194
215
|
return { data, error, setter };
|
|
195
216
|
};
|
|
196
217
|
const useSearchParamsHook = (errorRedirectUrl) => {
|
|
197
|
-
const navigate = useNavigate();
|
|
198
218
|
const { data, error, setter } = useSearchParamsRaw();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const redirectUrl = getErrorRedirectUrl(errorRedirectUrl);
|
|
202
|
-
navigate(redirectUrl, { replace: true });
|
|
203
|
-
}
|
|
204
|
-
}, [error, navigate, errorRedirectUrl]);
|
|
219
|
+
const redirectUrl = getErrorRedirectUrl(errorRedirectUrl);
|
|
220
|
+
useRedirect(redirectUrl, Boolean(error));
|
|
205
221
|
return [data ?? {}, setter];
|
|
206
222
|
};
|
|
207
223
|
return { useSearchParamsHook, useSearchParamsRaw };
|
|
@@ -234,14 +250,9 @@ var createRouteWithParams = (pattern, config) => {
|
|
|
234
250
|
return [void 0, result.issues];
|
|
235
251
|
};
|
|
236
252
|
const useParamsHook = () => {
|
|
237
|
-
const navigate = useNavigate();
|
|
238
253
|
const [params, error] = useParamsRaw();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const redirectUrl = getErrorRedirectUrl(config.errorRedirect);
|
|
242
|
-
navigate(redirectUrl, { replace: true });
|
|
243
|
-
}
|
|
244
|
-
}, [error, navigate]);
|
|
254
|
+
const redirectUrl = getErrorRedirectUrl(config.errorRedirect);
|
|
255
|
+
useRedirect(redirectUrl, Boolean(error));
|
|
245
256
|
return params ?? {};
|
|
246
257
|
};
|
|
247
258
|
return {
|
|
@@ -298,6 +309,8 @@ export {
|
|
|
298
309
|
URLParseError,
|
|
299
310
|
createRouteWithParams,
|
|
300
311
|
createRouteWithoutParams,
|
|
312
|
+
optionalSearchParams,
|
|
301
313
|
replaceState,
|
|
302
|
-
setGlobalErrorRedirect
|
|
314
|
+
setGlobalErrorRedirect,
|
|
315
|
+
useRedirect
|
|
303
316
|
};
|
package/dist/utils.d.ts
CHANGED
|
@@ -15,3 +15,25 @@ export declare const filterBySchemaKeys: <T extends v.ObjectSchema<any, any>>(ob
|
|
|
15
15
|
* Required parameters are kept as-is for subsequent validation.
|
|
16
16
|
*/
|
|
17
17
|
export declare const cleanOptionalParams: <T extends v.ObjectSchema<any, any>>(obj: Record<string, unknown>, schema: T) => Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Wraps all fields in a Valibot object schema with v.optional().
|
|
20
|
+
* Useful for search params where all fields should be optional by default.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // Instead of:
|
|
25
|
+
* searchParams: v.object({
|
|
26
|
+
* filter: v.optional(v.string()),
|
|
27
|
+
* sort: v.optional(v.string()),
|
|
28
|
+
* page: v.optional(v.number())
|
|
29
|
+
* })
|
|
30
|
+
*
|
|
31
|
+
* // You can write:
|
|
32
|
+
* searchParams: optionalSearchParams({
|
|
33
|
+
* filter: v.string(),
|
|
34
|
+
* sort: v.string(),
|
|
35
|
+
* page: v.number()
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare const optionalSearchParams: <TEntries extends v.ObjectEntries>(entries: TEntries) => v.ObjectSchema<any, undefined>;
|