@react-navigation/core 8.0.0-alpha.26 → 8.0.0-alpha.27

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.
Files changed (84) hide show
  1. package/lib/module/BaseNavigationContainer.js +12 -6
  2. package/lib/module/BaseNavigationContainer.js.map +1 -1
  3. package/lib/module/ConsumedParamsContext.js.map +1 -1
  4. package/lib/module/DataLoading.js +29 -4
  5. package/lib/module/DataLoading.js.map +1 -1
  6. package/lib/module/NavigationIndependentTree.js +8 -4
  7. package/lib/module/NavigationIndependentTree.js.map +1 -1
  8. package/lib/module/SceneView.js +16 -39
  9. package/lib/module/SceneView.js.map +1 -1
  10. package/lib/module/StaticNavigation.js +4 -3
  11. package/lib/module/StaticNavigation.js.map +1 -1
  12. package/lib/module/findFocusedRoute.js +1 -1
  13. package/lib/module/findFocusedRoute.js.map +1 -1
  14. package/lib/module/getActionFromState.js +1 -2
  15. package/lib/module/getActionFromState.js.map +1 -1
  16. package/lib/module/getPathFromState.js +41 -26
  17. package/lib/module/getPathFromState.js.map +1 -1
  18. package/lib/module/getPatternParts.js +15 -2
  19. package/lib/module/getPatternParts.js.map +1 -1
  20. package/lib/module/getStateFromPath.js +45 -19
  21. package/lib/module/getStateFromPath.js.map +1 -1
  22. package/lib/module/index.js +1 -0
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/useFocusEvents.js +6 -2
  25. package/lib/module/useFocusEvents.js.map +1 -1
  26. package/lib/module/useLazyValue.js.map +1 -1
  27. package/lib/module/useNavigationBuilder.js +8 -6
  28. package/lib/module/useNavigationBuilder.js.map +1 -1
  29. package/lib/module/useOnAction.js +6 -16
  30. package/lib/module/useOnAction.js.map +1 -1
  31. package/lib/module/useOnPreventRemove.js +1 -3
  32. package/lib/module/useOnPreventRemove.js.map +1 -1
  33. package/lib/module/useRouteCache.js +1 -3
  34. package/lib/module/useRouteCache.js.map +1 -1
  35. package/lib/module/useSyncState.js +3 -2
  36. package/lib/module/useSyncState.js.map +1 -1
  37. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  38. package/lib/typescript/src/ConsumedParamsContext.d.ts +1 -6
  39. package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -1
  40. package/lib/typescript/src/DataLoading.d.ts +1 -0
  41. package/lib/typescript/src/DataLoading.d.ts.map +1 -1
  42. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  43. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  44. package/lib/typescript/src/StaticNavigation.d.ts +39 -30
  45. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  46. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  47. package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
  48. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  49. package/lib/typescript/src/index.d.ts +2 -1
  50. package/lib/typescript/src/index.d.ts.map +1 -1
  51. package/lib/typescript/src/types.d.ts +2 -2
  52. package/lib/typescript/src/types.d.ts.map +1 -1
  53. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  54. package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
  55. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  56. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  57. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  58. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  59. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  60. package/lib/typescript/src/utilities.d.ts +10 -0
  61. package/lib/typescript/src/utilities.d.ts.map +1 -1
  62. package/package.json +5 -5
  63. package/src/BaseNavigationContainer.tsx +13 -7
  64. package/src/ConsumedParamsContext.tsx +1 -6
  65. package/src/DataLoading.tsx +68 -4
  66. package/src/NavigationIndependentTree.tsx +8 -5
  67. package/src/SceneView.tsx +18 -52
  68. package/src/StaticNavigation.tsx +88 -64
  69. package/src/findFocusedRoute.tsx +1 -1
  70. package/src/getActionFromState.tsx +7 -3
  71. package/src/getPathFromState.tsx +59 -32
  72. package/src/getPatternParts.tsx +15 -2
  73. package/src/getStateFromPath.tsx +77 -17
  74. package/src/index.tsx +5 -0
  75. package/src/types.tsx +2 -2
  76. package/src/useDescriptors.tsx +1 -1
  77. package/src/useFocusEvents.tsx +7 -5
  78. package/src/useLazyValue.tsx +2 -2
  79. package/src/useNavigationBuilder.tsx +12 -7
  80. package/src/useOnAction.tsx +11 -40
  81. package/src/useOnPreventRemove.tsx +4 -3
  82. package/src/useRouteCache.tsx +1 -4
  83. package/src/useSyncState.tsx +3 -2
  84. package/src/utilities.tsx +19 -0
@@ -39,7 +39,12 @@ type RouteConfig = {
39
39
  regex?: RegExp | undefined;
40
40
  pattern: string;
41
41
  segments: string[];
42
- params: { screen: string; name: string; index: number }[];
42
+ params: {
43
+ screen: string;
44
+ name: string;
45
+ index: number;
46
+ regex?: RegExp | undefined;
47
+ }[];
43
48
  routeNames: string[];
44
49
  parse?: ParseConfig | undefined;
45
50
  explicitParamNames?: Set<string> | undefined;
@@ -69,6 +74,7 @@ type ConfigResources = {
69
74
  configs: RouteConfig[];
70
75
  configsByScreen: Record<string, RouteConfig[]>;
71
76
  configsByPattern: Record<string, RouteConfig[]>;
77
+ prefixRegex?: RegExp | undefined;
72
78
  };
73
79
 
74
80
  const INVALID_SCHEMA_RESULT_ERROR =
@@ -88,6 +94,17 @@ const NESTED_SCREEN_PARAM_NAMES = [
88
94
  'pop',
89
95
  ];
90
96
 
97
+ const getStaticSegmentPattern = (segment: string) =>
98
+ Array.from(segment, (char) => {
99
+ const encoded = encodeURIComponent(char);
100
+ const percentEncoded =
101
+ encoded === char
102
+ ? `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`
103
+ : encoded;
104
+
105
+ return `(?:${escape(char)}|${escape(percentEncoded)})`;
106
+ }).join('');
107
+
91
108
  const getExplicitParamNames = (parse?: ParseConfig) => {
92
109
  const names: string[] = [];
93
110
 
@@ -161,8 +178,13 @@ export function getStateFromPath<ParamList extends {}>(
161
178
  options?: Options<ParamList>,
162
179
  previous?: NavigationState
163
180
  ): ResultState | undefined {
164
- const { initialRoutes, configs, configsByScreen, configsByPattern } =
165
- getConfigResources(options);
181
+ const {
182
+ initialRoutes,
183
+ configs,
184
+ configsByScreen,
185
+ configsByPattern,
186
+ prefixRegex,
187
+ } = getConfigResources(options);
166
188
 
167
189
  const screens = options?.screens;
168
190
 
@@ -183,24 +205,20 @@ export function getStateFromPath<ParamList extends {}>(
183
205
  let remaining = path
184
206
  .replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
185
207
  .replace(/^\//, '') // Remove extra leading slash
186
- .replace(/\?.*$/, ''); // Remove query params which we will handle later
208
+ .replace(/\?.*$/, '') // Remove query params which we will handle later
209
+ .replace(/%[0-9a-f]{2}/gi, (m) => m.toUpperCase()); // Normalize percent-encoding as hex digits are case-insensitive
187
210
 
188
211
  // Make sure there is a trailing slash
189
212
  remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
190
213
 
191
- const prefix = options?.path?.replace(/^\//, ''); // Remove extra leading slash
192
-
193
- if (prefix) {
194
- // Make sure there is a trailing slash
195
- const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
214
+ if (prefixRegex) {
215
+ const prefixMatch = remaining.match(prefixRegex);
196
216
 
197
- // If the path doesn't start with the prefix, it's not a match
198
- if (!remaining.startsWith(normalizedPrefix)) {
217
+ if (prefixMatch == null) {
199
218
  return undefined;
200
219
  }
201
220
 
202
- // Remove the prefix from the path
203
- remaining = remaining.replace(normalizedPrefix, '');
221
+ remaining = remaining.slice(prefixMatch[0].length);
204
222
  }
205
223
 
206
224
  if (screens === undefined) {
@@ -358,6 +376,19 @@ function getConfigResources<ParamList extends {}>(
358
376
  function prepareConfigResources(options?: Options<{}>) {
359
377
  const initialRoutes = getInitialRoutes(options);
360
378
  const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
379
+ const prefix = options?.path?.replace(/^\//, '');
380
+
381
+ let prefixRegex: RegExp | undefined;
382
+
383
+ if (prefix) {
384
+ const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
385
+ const prefixPattern = normalizedPrefix
386
+ .split('/')
387
+ .map(getStaticSegmentPattern)
388
+ .join('/');
389
+
390
+ prefixRegex = new RegExp(`^${prefixPattern}`);
391
+ }
361
392
 
362
393
  const configsByScreen: Record<string, RouteConfig[]> = {};
363
394
  const configsByPattern: Record<string, RouteConfig[]> = {};
@@ -380,6 +411,7 @@ function prepareConfigResources(options?: Options<{}>) {
380
411
  configs,
381
412
  configsByScreen,
382
413
  configsByPattern,
414
+ prefixRegex,
383
415
  };
384
416
  }
385
417
 
@@ -428,7 +460,21 @@ function getSortedNormalizedConfigs(
428
460
  // If 2 patterns are same, move the one with less route names up
429
461
  // This is an error state, so it's only useful for consistent error messages
430
462
  if (isArrayEqual(a.segments, b.segments)) {
431
- return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
463
+ if (
464
+ a.routeNames.length > b.routeNames.length &&
465
+ arrayStartsWith(a.routeNames, b.routeNames)
466
+ ) {
467
+ return -1;
468
+ }
469
+
470
+ if (
471
+ b.routeNames.length > a.routeNames.length &&
472
+ arrayStartsWith(b.routeNames, a.routeNames)
473
+ ) {
474
+ return 1;
475
+ }
476
+
477
+ return a.routeNames.length - b.routeNames.length || a.order - b.order;
432
478
  }
433
479
 
434
480
  // If one of the patterns starts with the other, it's more exhaustive
@@ -585,6 +631,12 @@ const matchAgainstConfig = (
585
631
  return undefined;
586
632
  }
587
633
 
634
+ // Percent-encoded values are matched permissively
635
+ // So validate the decoded value against the custom regex
636
+ if (param.regex && value !== decoded && !param.regex.test(decoded)) {
637
+ return undefined;
638
+ }
639
+
588
640
  const parser = routeConfig.parse?.[param.name];
589
641
 
590
642
  if (!parser) {
@@ -799,20 +851,27 @@ const createConfigItem = (
799
851
  segments.push(part.segment);
800
852
 
801
853
  if (part.param) {
802
- const reg = part.regex || '[^/]+';
854
+ // A custom regex may not match the percent-encoded form of the value
855
+ // So also accept segments containing an encoded character and validate them after decoding
856
+ const reg = part.regex
857
+ ? `(?:${part.regex})|(?=[^/]*%[0-9A-F]{2})[^/]+`
858
+ : '[^/]+';
803
859
 
804
860
  regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
805
861
  params.push({
806
862
  index,
807
863
  screen: part.screen,
808
864
  name: part.param,
865
+ regex: part.regex ? new RegExp(`^(?:${part.regex})$`) : undefined,
809
866
  });
810
867
 
811
868
  if (part.screen === screen) {
812
869
  pathParamNames.add(part.param);
813
870
  }
871
+ } else if (part.segment === '*') {
872
+ regexString += `.*\\/`;
814
873
  } else {
815
- regexString += `${part.segment === '*' ? '.*' : escape(part.segment)}\\/`;
874
+ regexString += `${getStaticSegmentPattern(part.segment)}\\/`;
816
875
  }
817
876
 
818
877
  index++;
@@ -1083,7 +1142,8 @@ const parseQueryParams = (
1083
1142
  ):
1084
1143
  | { valid: true; params?: Record<string, unknown> | undefined }
1085
1144
  | { valid: false } => {
1086
- const query = path.split('?')[1];
1145
+ const queryIndex = path.indexOf('?');
1146
+ const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
1087
1147
  const params: Record<string, unknown> = query ? queryString.parse(query) : {};
1088
1148
 
1089
1149
  // Path params should always win over same-named query params.
package/src/index.tsx CHANGED
@@ -11,6 +11,10 @@ export { getFocusedRouteNameFromRoute } from './getFocusedRouteNameFromRoute';
11
11
  export { getPathFromState } from './getPathFromState';
12
12
  export { getStateFromPath } from './getStateFromPath';
13
13
  export { NavigationContainerRefContext } from './NavigationContainerRefContext';
14
+ export {
15
+ type FocusedRouteState,
16
+ NavigationFocusedRouteStateContext,
17
+ } from './NavigationFocusedRouteStateContext';
14
18
  export { NavigationHelpersContext } from './NavigationHelpersContext';
15
19
  export { NavigationIndependentTree } from './NavigationIndependentTree';
16
20
  export { NavigationMetaContext } from './NavigationMetaContext';
@@ -61,6 +65,7 @@ export type {
61
65
  NavigatorTypeBagBase,
62
66
  NavigatorTypeBagFor,
63
67
  NestedNavigatorsForParamList,
68
+ ParamListForNavigator,
64
69
  PathConfig,
65
70
  PathConfigMap,
66
71
  RootNavigator,
package/src/types.tsx CHANGED
@@ -298,7 +298,7 @@ type NavigatorLike =
298
298
  getComponent: () => React.ComponentType<any>;
299
299
  };
300
300
 
301
- type ParamListForNavigator<T extends {}> = T extends NavigatorLike
301
+ export type ParamListForNavigator<T extends {}> = T extends NavigatorLike
302
302
  ? T extends PrivateValueStore<[infer ParamList extends {}, any, any, any]>
303
303
  ? ParamList
304
304
  : T
@@ -1654,7 +1654,7 @@ export type PathConfig<Params> = FlatType<
1654
1654
  path?: string | undefined;
1655
1655
  /**
1656
1656
  * An object mapping the param name to a function which converts the param value to a string.
1657
- * By default, all params are converted to strings using `String(value)`.
1657
+ * By default, arrays and null are preserved for query params, and other values are converted using `String(value)`.
1658
1658
  * Keys are constrained to valid param names when Params type is provided.
1659
1659
  *
1660
1660
  * @example
@@ -315,7 +315,7 @@ export function useDescriptors<
315
315
  render() {
316
316
  return element;
317
317
  },
318
- options: customOptions as ScreenOptions,
318
+ options: customOptions,
319
319
  };
320
320
 
321
321
  return acc;
@@ -51,7 +51,12 @@ export function useFocusEvents<State extends NavigationState>({
51
51
  React.useEffect(() => {
52
52
  const lastFocusedKey = lastFocusedKeyRef.current;
53
53
 
54
- lastFocusedKeyRef.current = currentFocusedKey;
54
+ const isFocused = navigation ? navigation.isFocused() : true;
55
+
56
+ if (isFocused) {
57
+ // Remember the current route as focused if the navigator is focused
58
+ lastFocusedKeyRef.current = currentFocusedKey;
59
+ }
55
60
 
56
61
  // We wouldn't have `lastFocusedKey` on initial mount
57
62
  // Fire focus event for the current route on mount if there's no parent navigator
@@ -61,10 +66,7 @@ export function useFocusEvents<State extends NavigationState>({
61
66
 
62
67
  // We should only emit events when the focused key changed and navigator is focused
63
68
  // When navigator is not focused, screens inside shouldn't receive focused status either
64
- if (
65
- lastFocusedKey === currentFocusedKey ||
66
- !(navigation ? navigation.isFocused() : true)
67
- ) {
69
+ if (lastFocusedKey === currentFocusedKey || !isFocused) {
68
70
  return;
69
71
  }
70
72
 
@@ -1,11 +1,11 @@
1
1
  import * as React from 'react';
2
2
 
3
- export function useLazyValue<T>(create: () => T) {
3
+ export function useLazyValue<T>(create: () => T): T {
4
4
  const lazyRef = React.useRef<T>(undefined);
5
5
 
6
6
  if (lazyRef.current === undefined) {
7
7
  lazyRef.current = create();
8
8
  }
9
9
 
10
- return lazyRef.current as T;
10
+ return lazyRef.current;
11
11
  }
@@ -337,7 +337,7 @@ export function useNavigationBuilder<
337
337
 
338
338
  const isNestedParamsConsumed =
339
339
  typeof route?.params === 'object' && route.params != null
340
- ? consumedParams?.isConsumed(route.params)
340
+ ? consumedParams?.has(route.params)
341
341
  : false;
342
342
 
343
343
  const {
@@ -571,7 +571,14 @@ export function useNavigationBuilder<
571
571
  return [stateBeforeInitialization, hydratedState, true, paramsForState];
572
572
  }
573
573
 
574
- return [undefined, hydratedState, false, paramsForState];
574
+ return [
575
+ undefined,
576
+ hydratedState,
577
+ false,
578
+ // Current state is already initialized
579
+ // So params weren't used, and need to be handled later
580
+ isStateInitialized(currentState) ? undefined : paramsForState,
581
+ ];
575
582
  }
576
583
  // We explicitly don't include routeNames, route.params etc. in the dep list
577
584
  // below. We want to avoid forcing a new state to be calculated in those cases
@@ -719,18 +726,16 @@ export function useNavigationBuilder<
719
726
  : nextState;
720
727
  }
721
728
 
722
- const setConsumedParams = consumedParams?.setConsumed;
723
-
724
729
  React.useEffect(() => {
725
730
  if (
726
- setConsumedParams &&
731
+ consumedParams &&
727
732
  didConsumeNestedParams &&
728
733
  typeof route?.params === 'object' &&
729
734
  route.params != null
730
735
  ) {
731
- setConsumedParams(route.params);
736
+ consumedParams.set(route.params, true);
732
737
  }
733
- }, [didConsumeNestedParams, route?.params, setConsumedParams]);
738
+ }, [consumedParams, didConsumeNestedParams, route?.params]);
734
739
 
735
740
  const shouldUpdate = state !== nextState;
736
741
 
@@ -1,16 +1,14 @@
1
1
  import type {
2
2
  NavigationAction,
3
- NavigationRoute,
4
3
  NavigationState,
5
- ParamListBase,
6
- PartialRoute,
7
4
  PartialState,
8
5
  Router,
9
6
  RouterConfigOptions,
10
7
  } from '@react-navigation/routers';
11
8
  import * as React from 'react';
12
9
 
13
- import { getLoaderForState } from './DataLoading';
10
+ import { ConsumedParamsContext } from './ConsumedParamsContext';
11
+ import { getLoaderForStateChange } from './DataLoading';
14
12
  import {
15
13
  type ChildActionListener,
16
14
  type ChildBeforeRemoveListener,
@@ -32,40 +30,6 @@ type Options<State extends NavigationState> = {
32
30
  emitter: NavigationEventEmitter<EventMapCore<any>>;
33
31
  };
34
32
 
35
- type MaybePartialRoute =
36
- | NavigationRoute<ParamListBase, string>
37
- | PartialRoute<NavigationRoute<ParamListBase, string>>;
38
-
39
- const didFocusedRouteChange = (
40
- currentState: NavigationState | PartialState<NavigationState>,
41
- nextState: NavigationState | PartialState<NavigationState>
42
- ) => {
43
- let currentFocusedRoute: MaybePartialRoute | undefined =
44
- currentState.routes[currentState.index ?? 0];
45
-
46
- let nextFocusedRoute: MaybePartialRoute | undefined =
47
- nextState.routes[nextState.index ?? 0];
48
-
49
- let changed = currentFocusedRoute?.key !== nextFocusedRoute?.key;
50
-
51
- while (
52
- !changed &&
53
- currentFocusedRoute?.state != null &&
54
- nextFocusedRoute?.state != null &&
55
- currentFocusedRoute.state !== nextFocusedRoute.state
56
- ) {
57
- currentFocusedRoute =
58
- currentFocusedRoute.state.routes[currentFocusedRoute.state.index ?? 0];
59
-
60
- nextFocusedRoute =
61
- nextFocusedRoute.state.routes[nextFocusedRoute.state.index ?? 0];
62
-
63
- changed = currentFocusedRoute?.key !== nextFocusedRoute?.key;
64
- }
65
-
66
- return changed;
67
- };
68
-
69
33
  /**
70
34
  * Hook to handle actions for a navigator, including state updates and bubbling.
71
35
  *
@@ -93,6 +57,7 @@ export function useOnAction<State extends NavigationState>({
93
57
  } = React.use(NavigationBuilderContext);
94
58
 
95
59
  const tree = React.use(StaticTreeContext);
60
+ const consumedParams = React.use(ConsumedParamsContext);
96
61
 
97
62
  const routerConfigOptionsRef =
98
63
  React.useRef<RouterConfigOptions>(routerConfigOptions);
@@ -146,8 +111,13 @@ export function useOnAction<State extends NavigationState>({
146
111
 
147
112
  onDispatchAction(action, false);
148
113
 
149
- if (tree && didFocusedRouteChange(state, result)) {
150
- const loader = getLoaderForState(tree, result);
114
+ if (tree) {
115
+ const loader = getLoaderForStateChange(
116
+ tree,
117
+ result,
118
+ state,
119
+ consumedParams
120
+ );
151
121
 
152
122
  loader?.();
153
123
  }
@@ -194,6 +164,7 @@ export function useOnAction<State extends NavigationState>({
194
164
  [
195
165
  actionListeners,
196
166
  beforeRemoveListeners,
167
+ consumedParams,
197
168
  emitter,
198
169
  getState,
199
170
  key,
@@ -39,9 +39,10 @@ export const shouldPreventRemove = (
39
39
  .filter(([, route]) => route.key != null)
40
40
  );
41
41
 
42
- const visitedRouteKeys: Set<string> =
43
- // @ts-expect-error: add this property to mark that we've already emitted this action
44
- action[VISITED_ROUTE_KEYS] ?? new Set<string>();
42
+ const visitedRouteKeys =
43
+ VISITED_ROUTE_KEYS in action && action[VISITED_ROUTE_KEYS] instanceof Set
44
+ ? action[VISITED_ROUTE_KEYS]
45
+ : new Set<string>();
45
46
 
46
47
  const beforeRemoveAction = {
47
48
  ...action,
@@ -41,10 +41,7 @@ export function useRouteCache<State extends NavigationState>(
41
41
  // FIXME: since the state is updated with mutation, the route object cannot be frozen
42
42
  // As a workaround, loop through the object and make the properties readonly
43
43
  // Only needed once per proxy - skip if we're reusing a previously-frozen one
44
- for (const key in proxy) {
45
- // @ts-expect-error: this is fine since we are looping through the object
46
- const value = proxy[key];
47
-
44
+ for (const [key, value] of Object.entries(proxy)) {
48
45
  Object.defineProperty(proxy, key, {
49
46
  enumerable: true,
50
47
  configurable: true,
@@ -26,9 +26,10 @@ const createStore = <T,>(getInitialState: () => T) => {
26
26
 
27
27
  const setState = (newState: T) => {
28
28
  state = deepFreeze(newState);
29
- didUpdate = true;
30
29
 
31
- if (!isBatching) {
30
+ if (isBatching) {
31
+ didUpdate = true;
32
+ } else {
32
33
  listeners.forEach((listener) => listener());
33
34
  }
34
35
  };
package/src/utilities.tsx CHANGED
@@ -26,6 +26,25 @@ export type NoExcessObject<Provided, Allowed> = Record<
26
26
  never
27
27
  >;
28
28
 
29
+ /**
30
+ * Check an object captured as a generic type against an allowed type,
31
+ * e.g. an object returned from a callback isn't checked for excess properties.
32
+ */
33
+ export type NoExcessCallbackReturn<
34
+ Provided extends {},
35
+ Allowed extends {},
36
+ > = Allowed & Provided & NoExcessObject<Provided, Allowed>;
37
+
38
+ /**
39
+ * Apply excess property checking to a value,
40
+ * or to the return value if the value is a function.
41
+ */
42
+ export type NoExcessValue<Provided, Allowed extends {}> = Provided extends (
43
+ ...args: never[]
44
+ ) => infer Return
45
+ ? (...args: never[]) => Allowed & NoExcessObject<Return, Allowed>
46
+ : NoExcessObject<Provided, Allowed>;
47
+
29
48
  /**
30
49
  * We get a union type when using keyof, but we want an intersection instead.
31
50
  * https://stackoverflow.com/a/50375286/1665026