@@ -281,6 +281,9 @@ let localIdCounter: number = 0;
281281let thenableIndexCounter : number = 0 ;
282282let thenableState : ThenableState | null = null ;
283283
284+ // Track whether we've switched dispatchers due to conditional use
285+ let hasDispatcherSwitchedDueToUse : boolean = false ;
286+
284287// Used for ids that are generated completely client-side (i.e. not during
285288// hydration). This counter is global, so client ids are not stable across
286289// render attempts.
@@ -624,6 +627,9 @@ export function renderWithHooks<Props, SecondArg>(
624627
625628 finishRenderingHooks ( current , workInProgress , Component ) ;
626629
630+ // Reset dispatcher switch flag for the next render
631+ hasDispatcherSwitchedDueToUse = false ;
632+
627633 return children ;
628634}
629635
@@ -931,6 +937,9 @@ export function resetHooksAfterThrow(): void {
931937 // We can assume the previous dispatcher is always this one, since we set it
932938 // at the beginning of the render phase and there's no re-entrance.
933939 ReactSharedInternals . H = ContextOnlyDispatcher ;
940+
941+ // Reset dispatcher switch flag
942+ hasDispatcherSwitchedDueToUse = false ;
934943}
935944
936945export function resetHooksOnUnwind ( workInProgress : Fiber ) : void {
@@ -1037,6 +1046,32 @@ function updateWorkInProgressHook(): Hook {
10371046 'Update hook called on initial render. This is likely a bug in React. Please file an issue.' ,
10381047 ) ;
10391048 } 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+
10401075 // This is an update. We should always have a current hook.
10411076 throw new Error ( 'Rendered more hooks than during the previous render.' ) ;
10421077 }
@@ -1130,11 +1165,15 @@ function useThenable<T>(thenable: Thenable<T>): T {
11301165 const currentFiber = workInProgressFiber . alternate ;
11311166 if ( __DEV__ ) {
11321167 if ( currentFiber !== null && currentFiber . memoizedState !== null ) {
1168+ hasDispatcherSwitchedDueToUse = true ;
11331169 ReactSharedInternals . H = HooksDispatcherOnUpdateInDEV ;
11341170 } else {
11351171 ReactSharedInternals . H = HooksDispatcherOnMountInDEV ;
11361172 }
11371173 } else {
1174+ if ( currentFiber !== null && currentFiber . memoizedState !== null ) {
1175+ hasDispatcherSwitchedDueToUse = true ;
1176+ }
11381177 ReactSharedInternals.H =
11391178 currentFiber === null || currentFiber.memoizedState === null
11401179 ? HooksDispatcherOnMount
@@ -1301,13 +1340,36 @@ function updateReducerImpl<S, A>(
13011340 current: Hook,
13021341 reducer: (S, A) => S ,
13031342) : [ S , Dispatch < A > ] {
1304- const queue = hook . queue ;
1343+ let queue = hook . queue ;
13051344
13061345 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 ( hasDispatcherSwitchedDueToUse && hook . memoizedState === null ) {
1348+ // Initialize the hook for conditional use case
1349+ // For conditional use, we don't have the initial state, so we use the current state
1350+ // or a default value. This matches what would happen during mount.
1351+ const initialState = ( ( undefined : any ) : S ) ;
1352+ hook . memoizedState = hook . baseState = initialState ;
1353+ const newQueue : UpdateQueue < S , A > = {
1354+ pending : null ,
1355+ lanes : NoLanes ,
1356+ dispatch : null ,
1357+ lastRenderedReducer : reducer ,
1358+ lastRenderedState : initialState ,
1359+ } ;
1360+ queue = hook . queue = newQueue ;
1361+ const dispatch : Dispatch < A > = (dispatchReducerAction.bind(
1362+ null,
1363+ currentlyRenderingFiber,
1364+ newQueue,
1365+ ): any);
1366+ newQueue.dispatch = dispatch;
1367+ } else {
1368+ throw new Error (
1369+ 'Should have a queue. You are likely calling Hooks conditionally, ' +
1370+ 'which is not allowed. (https://react.dev/link/invalid-hook-call)' ,
1371+ ) ;
1372+ }
13111373 }
13121374
13131375 queue . lastRenderedReducer = reducer ;
@@ -2900,6 +2962,14 @@ function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
29002962 const hook = updateWorkInProgressHook ( ) ;
29012963 const nextDeps = deps === undefined ? null : deps ;
29022964 const prevState = hook . memoizedState ;
2965+
2966+ // Handle conditional use scenario where hook was newly created
2967+ if ( hasDispatcherSwitchedDueToUse && prevState === null ) {
2968+ // This hook was created due to conditional use - initialize it properly
2969+ hook . memoizedState = [ callback , nextDeps ] ;
2970+ return callback ;
2971+ }
2972+
29032973 if (nextDeps !== null) {
29042974 const prevDeps : Array < mixed > | null = prevState [ 1 ] ;
29052975 if ( areHookInputsEqual ( nextDeps , prevDeps ) ) {
@@ -2936,6 +3006,23 @@ function updateMemo<T>(
29363006 const hook = updateWorkInProgressHook ( ) ;
29373007 const nextDeps = deps === undefined ? null : deps ;
29383008 const prevState = hook . memoizedState ;
3009+
3010+ // Handle conditional use scenario where hook was newly created
3011+ if ( hasDispatcherSwitchedDueToUse && prevState === null ) {
3012+ // This hook was created due to conditional use - initialize it properly
3013+ const nextValue = nextCreate ( ) ;
3014+ if ( shouldDoubleInvokeUserFnsInHooksDEV ) {
3015+ setIsStrictModeForDevtools ( true ) ;
3016+ try {
3017+ nextCreate ( ) ;
3018+ } finally {
3019+ setIsStrictModeForDevtools ( false ) ;
3020+ }
3021+ }
3022+ hook . memoizedState = [ nextValue , nextDeps ] ;
3023+ return nextValue ;
3024+ }
3025+
29393026 // Assume these are defined. If they're not, areHookInputsEqual will warn.
29403027 if ( nextDeps !== null ) {
29413028 const prevDeps : Array < mixed > | null = prevState [ 1 ] ;
0 commit comments