@idealyst/navigation 1.1.4 → 1.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/navigation",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Cross-platform navigation library for React and React Native",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
@@ -43,8 +43,8 @@
43
43
  "publish:npm": "npm publish"
44
44
  },
45
45
  "peerDependencies": {
46
- "@idealyst/components": "^1.1.4",
47
- "@idealyst/theme": "^1.1.4",
46
+ "@idealyst/components": "^1.1.5",
47
+ "@idealyst/theme": "^1.1.5",
48
48
  "@react-navigation/bottom-tabs": ">=7.0.0",
49
49
  "@react-navigation/drawer": ">=7.0.0",
50
50
  "@react-navigation/native": ">=7.0.0",
@@ -60,10 +60,10 @@
60
60
  "react-router-dom": ">=6.0.0"
61
61
  },
62
62
  "devDependencies": {
63
- "@idealyst/components": "^1.1.4",
63
+ "@idealyst/components": "^1.1.5",
64
64
  "@idealyst/datagrid": "^1.0.93",
65
65
  "@idealyst/datepicker": "^1.0.93",
66
- "@idealyst/theme": "^1.1.4",
66
+ "@idealyst/theme": "^1.1.5",
67
67
  "@types/react": "^19.1.8",
68
68
  "@types/react-dom": "^19.1.6",
69
69
  "react": "^19.1.0",
@@ -198,17 +198,23 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
198
198
  return;
199
199
  }
200
200
 
201
+ // Merge route params with navigation state (state values stored directly in params)
202
+ const navigationParams = {
203
+ ...parsed.params,
204
+ ...(params.state || {}),
205
+ };
206
+
201
207
  if (params.replace) {
202
208
  // Use CommonActions.reset to replace the current route
203
209
  navigation.dispatch(
204
210
  CommonActions.reset({
205
211
  index: 0,
206
- routes: [{ name: parsed.routeName, params: parsed.params }],
212
+ routes: [{ name: parsed.routeName, params: navigationParams }],
207
213
  })
208
214
  );
209
215
  } else {
210
216
  // Navigate to the pattern route with extracted parameters
211
- navigation.navigate(parsed.routeName as never, parsed.params as never);
217
+ navigation.navigate(parsed.routeName as never, navigationParams as never);
212
218
  }
213
219
  };
214
220
 
@@ -290,17 +296,23 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
290
296
  return;
291
297
  }
292
298
 
299
+ // Merge route params with navigation state (state values stored directly in params)
300
+ const navigationParams = {
301
+ ...parsed.params,
302
+ ...(params.state || {}),
303
+ };
304
+
293
305
  if (params.replace) {
294
306
  // Use CommonActions.reset to replace the current route
295
307
  navigation.dispatch(
296
308
  CommonActions.reset({
297
309
  index: 0,
298
- routes: [{ name: parsed.routeName, params: parsed.params }],
310
+ routes: [{ name: parsed.routeName, params: navigationParams }],
299
311
  })
300
312
  );
301
313
  } else {
302
314
  // Navigate to the pattern route with extracted parameters
303
- navigation.navigate(parsed.routeName as never, parsed.params as never);
315
+ navigation.navigate(parsed.routeName as never, navigationParams as never);
304
316
  }
305
317
  };
306
318
 
@@ -238,8 +238,23 @@ export const NavigatorProvider = ({
238
238
  // If no notFoundComponent configured, React Router will render nothing
239
239
  }
240
240
 
241
- // Use React Router's navigate function with replace option
242
- reactRouterNavigate(path, { replace: params.replace });
241
+ // Build URL with query params if state is provided
242
+ let finalPath = path;
243
+ if (params.state && Object.keys(params.state).length > 0) {
244
+ const searchParams = new URLSearchParams();
245
+ for (const [key, value] of Object.entries(params.state)) {
246
+ if (value !== undefined && value !== null) {
247
+ searchParams.set(key, String(value));
248
+ }
249
+ }
250
+ const queryString = searchParams.toString();
251
+ if (queryString) {
252
+ finalPath = `${path}?${queryString}`;
253
+ }
254
+ }
255
+
256
+ // Use React Router's navigate function
257
+ reactRouterNavigate(finalPath, { replace: params.replace });
243
258
  }
244
259
  };
245
260
 
@@ -11,6 +11,16 @@ export type NavigateParams = {
11
11
  * On web, this uses history.replace(). On native, this resets the navigation state.
12
12
  */
13
13
  replace?: boolean;
14
+ /**
15
+ * Optional state data to pass to the destination screen.
16
+ * On web, this is passed as URL query parameters (e.g., ?autostart=true).
17
+ * On native, this is passed via route params.
18
+ * Use useNavigationState() hook to access this data in the destination screen.
19
+ *
20
+ * Note: Values are serialized to strings. Booleans and numbers are automatically
21
+ * parsed back when using useNavigationState().
22
+ */
23
+ state?: Record<string, string | number | boolean>;
14
24
  };
15
25
 
16
26
  export type NavigatorProviderProps = {
@@ -1,2 +1,3 @@
1
1
  // Native-specific hook exports
2
- export * from './useParams.native';
2
+ export * from './useParams.native';
3
+ export * from './useNavigationState.native';
@@ -1,2 +1,3 @@
1
1
  // Cross-platform hook exports
2
- export * from './useParams.web'; // This will be overridden by platform-specific exports
2
+ export * from './useParams.web'; // This will be overridden by platform-specific exports
3
+ export * from './useNavigationState.web'; // This will be overridden by platform-specific exports
@@ -1,2 +1,3 @@
1
1
  // Web-specific hook exports
2
- export * from './useParams.web';
2
+ export * from './useParams.web';
3
+ export * from './useNavigationState.web';
@@ -0,0 +1,34 @@
1
+ import { useRoute } from '@react-navigation/native';
2
+
3
+ export interface UseNavigationStateOptions {
4
+ /**
5
+ * Keys to remove from the URL query parameters after reading.
6
+ * This option only affects web - it has no effect on native.
7
+ * Included here for API consistency across platforms.
8
+ */
9
+ consume?: string[];
10
+ }
11
+
12
+ /**
13
+ * Hook to access navigation state passed via the navigate() function.
14
+ * On native, state is passed via route params (merged with path params).
15
+ * Returns the state object passed during navigation, or an empty object if no state was passed.
16
+ *
17
+ * @example
18
+ * // Navigate with state
19
+ * navigate({ path: '/recording', state: { autostart: true } });
20
+ *
21
+ * // Access state in destination screen
22
+ * const { autostart } = useNavigationState<{ autostart?: boolean }>();
23
+ *
24
+ * @example
25
+ * // The consume option is accepted but has no effect on native
26
+ * const { autostart } = useNavigationState<{ autostart?: boolean }>({ consume: ['autostart'] });
27
+ */
28
+ export function useNavigationState<T extends Record<string, unknown> = Record<string, unknown>>(
29
+ _options?: UseNavigationStateOptions
30
+ ): T {
31
+ const route = useRoute();
32
+ const params = route.params as Record<string, unknown> | undefined;
33
+ return (params as T) || ({} as T);
34
+ }
@@ -0,0 +1,87 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useSearchParams } from '../router';
3
+
4
+ export interface UseNavigationStateOptions {
5
+ /**
6
+ * Keys to remove from the URL query parameters after reading.
7
+ * This is useful for one-time state like 'autostart' that shouldn't persist in the URL.
8
+ * Only affects web - has no effect on native.
9
+ *
10
+ * @example
11
+ * // URL: /recording?autostart=true
12
+ * const { autostart } = useNavigationState<{ autostart?: boolean }>({ consume: ['autostart'] });
13
+ * // autostart = true, URL becomes: /recording
14
+ */
15
+ consume?: string[];
16
+ }
17
+
18
+ /**
19
+ * Hook to access navigation state passed via the navigate() function.
20
+ * On web, state is passed as URL query parameters.
21
+ * Returns the state object passed during navigation, or an empty object if no state was passed.
22
+ *
23
+ * @example
24
+ * // Navigate with state
25
+ * navigate({ path: '/recording', state: { autostart: true } });
26
+ *
27
+ * // Access state in destination screen
28
+ * const { autostart } = useNavigationState<{ autostart?: boolean }>();
29
+ * // URL will be: /recording?autostart=true
30
+ *
31
+ * @example
32
+ * // Consume (remove) the parameter after reading
33
+ * const { autostart } = useNavigationState<{ autostart?: boolean }>({ consume: ['autostart'] });
34
+ * // autostart = true, URL becomes: /recording (param removed)
35
+ */
36
+ function parseSearchParams(searchParams: URLSearchParams): Record<string, unknown> {
37
+ const state: Record<string, unknown> = {};
38
+ searchParams.forEach((value, key) => {
39
+ if (value === 'true') {
40
+ state[key] = true;
41
+ } else if (value === 'false') {
42
+ state[key] = false;
43
+ } else if (!isNaN(Number(value)) && value !== '') {
44
+ state[key] = Number(value);
45
+ } else {
46
+ state[key] = value;
47
+ }
48
+ });
49
+ return state;
50
+ }
51
+
52
+ export function useNavigationState<T extends Record<string, unknown> = Record<string, unknown>>(
53
+ options?: UseNavigationStateOptions
54
+ ): T {
55
+ const [searchParams, setSearchParams] = useSearchParams();
56
+ const consumedStateRef = useRef<Record<string, unknown> | null>(null);
57
+ const hasConsumed = useRef(false);
58
+
59
+ // Capture consumed values before they're removed from URL
60
+ if (options?.consume && options.consume.length > 0 && !hasConsumed.current) {
61
+ const keysToConsume = options.consume.filter(key => searchParams.has(key));
62
+ if (keysToConsume.length > 0 && consumedStateRef.current === null) {
63
+ // Capture the full state including values we're about to consume
64
+ consumedStateRef.current = parseSearchParams(searchParams);
65
+ }
66
+ }
67
+
68
+ // Remove consumed keys from URL after initial read
69
+ useEffect(() => {
70
+ if (options?.consume && options.consume.length > 0 && !hasConsumed.current) {
71
+ const keysToRemove = options.consume.filter(key => searchParams.has(key));
72
+ if (keysToRemove.length > 0) {
73
+ hasConsumed.current = true;
74
+ const newParams = new URLSearchParams(searchParams);
75
+ keysToRemove.forEach(key => newParams.delete(key));
76
+ setSearchParams(newParams, { replace: true });
77
+ }
78
+ }
79
+ }, [options?.consume, searchParams, setSearchParams]);
80
+
81
+ // Return captured state if we consumed params, otherwise parse current URL
82
+ if (consumedStateRef.current !== null) {
83
+ return consumedStateRef.current as T;
84
+ }
85
+
86
+ return parseSearchParams(searchParams) as T;
87
+ }