@@ -281,6 +281,9 @@ let localIdCounter: number = 0;
281
281
let thenableIndexCounter : number = 0 ;
282
282
let thenableState : ThenableState | null = null ;
283
283
284
+ // Track whether we've switched dispatchers due to conditional use
285
+ let hasDispatcherSwitchedDueToUse : boolean = false ;
286
+
284
287
// Used for ids that are generated completely client-side (i.e. not during
285
288
// hydration). This counter is global, so client ids are not stable across
286
289
// render attempts.
@@ -624,6 +627,9 @@ export function renderWithHooks<Props, SecondArg>(
624
627
625
628
finishRenderingHooks ( current , workInProgress , Component ) ;
626
629
630
+ // Reset dispatcher switch flag for the next render
631
+ hasDispatcherSwitchedDueToUse = false ;
632
+
627
633
return children ;
628
634
}
629
635
@@ -931,6 +937,9 @@ export function resetHooksAfterThrow(): void {
931
937
// We can assume the previous dispatcher is always this one, since we set it
932
938
// at the beginning of the render phase and there's no re-entrance.
933
939
ReactSharedInternals . H = ContextOnlyDispatcher ;
940
+
941
+ // Reset dispatcher switch flag
942
+ hasDispatcherSwitchedDueToUse = false ;
934
943
}
935
944
936
945
export function resetHooksOnUnwind ( workInProgress : Fiber ) : void {
@@ -1037,6 +1046,32 @@ function updateWorkInProgressHook(): Hook {
1037
1046
'Update hook called on initial render. This is likely a bug in React. Please file an issue.' ,
1038
1047
) ;
1039
1048
} else {
1049
+ // Check if we're in a conditional use scenario
1050
+ if ( hasDispatcherSwitchedDueToUse ) {
1051
+ // We're in a situation where conditional use has caused hook count mismatch.
1052
+ // Create a new hook and mark that we should treat the next hook as a mount.
1053
+ const newHook : Hook = {
1054
+ memoizedState : null ,
1055
+ baseState : null ,
1056
+ baseQueue : null ,
1057
+ queue : null ,
1058
+ next : null ,
1059
+ } ;
1060
+
1061
+ if ( workInProgressHook === null ) {
1062
+ // This is the first hook in the list.
1063
+ currentlyRenderingFiber . memoizedState = workInProgressHook =
1064
+ newHook ;
1065
+ } else {
1066
+ // Append to the end of the list.
1067
+ workInProgressHook = workInProgressHook . next = newHook ;
1068
+ }
1069
+
1070
+ // Don't throw error - let the hook continue
1071
+ // The specific hook implementation will handle initialization
1072
+ return workInProgressHook ;
1073
+ }
1074
+
1040
1075
// This is an update. We should always have a current hook.
1041
1076
throw new Error ( 'Rendered more hooks than during the previous render.' ) ;
1042
1077
}
@@ -1130,11 +1165,15 @@ function useThenable<T>(thenable: Thenable<T>): T {
1130
1165
const currentFiber = workInProgressFiber . alternate ;
1131
1166
if ( __DEV__ ) {
1132
1167
if ( currentFiber !== null && currentFiber . memoizedState !== null ) {
1168
+ hasDispatcherSwitchedDueToUse = true ;
1133
1169
ReactSharedInternals . H = HooksDispatcherOnUpdateInDEV ;
1134
1170
} else {
1135
1171
ReactSharedInternals . H = HooksDispatcherOnMountInDEV ;
1136
1172
}
1137
1173
} else {
1174
+ if ( currentFiber !== null && currentFiber . memoizedState !== null ) {
1175
+ hasDispatcherSwitchedDueToUse = true ;
1176
+ }
1138
1177
ReactSharedInternals.H =
1139
1178
currentFiber === null || currentFiber.memoizedState === null
1140
1179
? HooksDispatcherOnMount
@@ -1304,10 +1343,20 @@ function updateReducerImpl<S, A>(
1304
1343
const queue = hook . queue ;
1305
1344
1306
1345
if ( queue === null ) {
1307
- throw new Error (
1308
- 'Should have a queue. You are likely calling Hooks conditionally, ' +
1309
- 'which is not allowed. (https://react.dev/link/invalid-hook-call)' ,
1310
- ) ;
1346
+ // Check if this is a conditional use scenario
1347
+ if ( handleConditionalUseInit ( hook ) ) {
1348
+ // For conditional use, delegate to mount behavior with default initial value
1349
+ const initialState =
1350
+ reducer === basicStateReducer ? false : ( ( undefined : any ) : S ) ;
1351
+ return callWithConditionalUseFlag ( ( ) =>
1352
+ mountReducer ( reducer , initialState , undefined ) ,
1353
+ ) ;
1354
+ } else {
1355
+ throw new Error (
1356
+ 'Should have a queue. You are likely calling Hooks conditionally, ' +
1357
+ 'which is not allowed. (https://react.dev/link/invalid-hook-call)' ,
1358
+ ) ;
1359
+ }
1311
1360
}
1312
1361
1313
1362
queue . lastRenderedReducer = reducer ;
@@ -1761,6 +1810,14 @@ function updateSyncExternalStore<T>(
1761
1810
}
1762
1811
const inst = hook.queue;
1763
1812
1813
+ // Handle conditional use scenario where hook was newly created
1814
+ if (handleConditionalUseInit(hook) && inst === null ) {
1815
+ // Delegate to mount behavior
1816
+ return callWithConditionalUseFlag ( ( ) =>
1817
+ mountSyncExternalStore ( subscribe , getSnapshot , getServerSnapshot ) ,
1818
+ ) ;
1819
+ }
1820
+
1764
1821
updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [
1765
1822
subscribe,
1766
1823
]);
@@ -2633,6 +2690,15 @@ function updateEffectImpl(
2633
2690
): void {
2634
2691
const hook = updateWorkInProgressHook ( ) ;
2635
2692
const nextDeps = deps === undefined ? null : deps ;
2693
+
2694
+ // Handle conditional use scenario where hook was newly created
2695
+ if ( handleConditionalUseInit ( hook ) ) {
2696
+ // Delegate to mount behavior
2697
+ return callWithConditionalUseFlag ( ( ) =>
2698
+ mountEffectImpl ( fiberFlags , hookFlags , create , deps ) ,
2699
+ ) ;
2700
+ }
2701
+
2636
2702
const effect : Effect = hook . memoizedState ;
2637
2703
const inst = effect . inst ;
2638
2704
@@ -2900,6 +2966,13 @@ function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
2900
2966
const hook = updateWorkInProgressHook ( ) ;
2901
2967
const nextDeps = deps === undefined ? null : deps ;
2902
2968
const prevState = hook . memoizedState ;
2969
+
2970
+ // Handle conditional use scenario where hook was newly created
2971
+ if ( handleConditionalUseInit ( hook ) ) {
2972
+ // Delegate to mount behavior
2973
+ return callWithConditionalUseFlag ( ( ) => mountCallback ( callback , deps ) ) ;
2974
+ }
2975
+
2903
2976
if ( nextDeps !== null ) {
2904
2977
const prevDeps : Array < mixed > | null = prevState [ 1 ] ;
2905
2978
if ( areHookInputsEqual ( nextDeps , prevDeps ) ) {
@@ -2929,13 +3002,36 @@ function mountMemo<T>(
2929
3002
return nextValue;
2930
3003
}
2931
3004
3005
+ // Helper function to handle conditional use initialization
3006
+ function handleConditionalUseInit ( hook : Hook ) : boolean {
3007
+ return hasDispatcherSwitchedDueToUse && hook . memoizedState === null ;
3008
+ }
3009
+
3010
+ // Wrapper to safely call mount functions during conditional use
3011
+ function callWithConditionalUseFlag< T > (fn: () => T ) : T {
3012
+ const prevFlag = hasDispatcherSwitchedDueToUse ;
3013
+ hasDispatcherSwitchedDueToUse = false ;
3014
+ try {
3015
+ return fn ( ) ;
3016
+ } finally {
3017
+ hasDispatcherSwitchedDueToUse = prevFlag ;
3018
+ }
3019
+ }
3020
+
2932
3021
function updateMemo < T > (
2933
3022
nextCreate : ( ) = > T ,
2934
3023
deps : Array < mixed > | void | null,
2935
3024
): T {
2936
3025
const hook = updateWorkInProgressHook ( ) ;
2937
3026
const nextDeps = deps === undefined ? null : deps ;
2938
3027
const prevState = hook . memoizedState ;
3028
+
3029
+ // Handle conditional use scenario where hook was newly created
3030
+ if ( handleConditionalUseInit ( hook ) ) {
3031
+ // Delegate to mount behavior
3032
+ return callWithConditionalUseFlag ( ( ) => mountMemo ( nextCreate , deps ) ) ;
3033
+ }
3034
+
2939
3035
// Assume these are defined. If they're not, areHookInputsEqual will warn.
2940
3036
if ( nextDeps !== null ) {
2941
3037
const prevDeps : Array < mixed > | null = prevState [ 1 ] ;
@@ -3401,6 +3497,27 @@ function updateTransition(): [
3401
3497
const [ booleanOrThenable ] = updateState ( false ) ;
3402
3498
const hook = updateWorkInProgressHook ( ) ;
3403
3499
const start = hook . memoizedState ;
3500
+
3501
+ // Handle conditional use scenario
3502
+ if ( handleConditionalUseInit ( hook ) ) {
3503
+ // Delegate to mount behavior
3504
+ return callWithConditionalUseFlag ( ( ) => mountTransition ( ) ) ;
3505
+ }
3506
+
3507
+ // Check if start is null for other error conditions
3508
+ if ( start === null ) {
3509
+ const stateHook = currentlyRenderingFiber . memoizedState ;
3510
+ const startFn = startTransition . bind (
3511
+ null ,
3512
+ currentlyRenderingFiber ,
3513
+ stateHook . queue ,
3514
+ true ,
3515
+ false ,
3516
+ ) ;
3517
+ hook . memoizedState = startFn ;
3518
+ return [ false , startFn ] ;
3519
+ }
3520
+
3404
3521
const isPending =
3405
3522
typeof booleanOrThenable === 'boolean'
3406
3523
? booleanOrThenable
0 commit comments