@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 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: v.object({
31
- tab: v.optional(v.string()),
32
- page: v.optional(v.pipe(v.string(), v.transform(Number), v.number())),
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
- useEffect(() => {
200
- if (error) {
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
- useEffect(() => {
240
- if (error) {
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovineko/react-router",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Type-safe wrapper for React Router v7 with typed params, query params, and Link components",
5
5
  "keywords": [
6
6
  "react",