From 3203a10942f85d1aa9ebb4e12276b527a319b6c5 Mon Sep 17 00:00:00 2001 From: Ravindra Methaniya Date: Fri, 18 Jul 2025 12:29:26 +0530 Subject: [PATCH 1/2] Add support for useSetStateWithCallback in React Hooks This commit introduces the useSetStateWithCallback hook across various React components, allowing state updates with an optional callback. The hook is implemented in the React reconciler and debug tools, with appropriate error handling for unsupported scenarios in server components. Additionally, the dispatcher types have been updated to include this new hook, ensuring compatibility with existing functionality. --- .../react-debug-tools/src/ReactDebugHooks.js | 5 ++ .../react-reconciler/src/ReactFiberHooks.js | 66 ++++++++++++++++++- .../src/ReactInternalTypes.js | 4 +- packages/react-server/src/ReactFizzHooks.js | 6 ++ packages/react-server/src/ReactFlightHooks.js | 5 ++ 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 8242b27d4e5be..9863e2bae044a 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -749,6 +749,10 @@ function useEffectEvent) => mixed>(callback: F): F { return callback; } +function unsupportedSetStateWithCallback() { + throw new Error('useSetStateWithCallback is not supported in React Debug Tools.'); +} + const Dispatcher: DispatcherType = { readContext, @@ -775,6 +779,7 @@ const Dispatcher: DispatcherType = { useMemoCache, useCacheRefresh, useEffectEvent, + useSetStateWithCallback: unsupportedSetStateWithCallback, }; // create a proxy to throw a custom error diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 63332124e2f0b..7064751f65ef1 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -3897,6 +3897,7 @@ const HooksDispatcherOnMount: Dispatcher = { useOptimistic: mountOptimistic, useMemoCache, useCacheRefresh: mountRefresh, + useSetStateWithCallback: mountSetStateWithCallback, }; if (enableUseEffectEventHook) { (HooksDispatcherOnMount: Dispatcher).useEffectEvent = mountEvent; @@ -3927,6 +3928,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { useOptimistic: updateOptimistic, useMemoCache, useCacheRefresh: updateRefresh, + useSetStateWithCallback: updateSetStateWithCallback, }; if (enableUseEffectEventHook) { (HooksDispatcherOnUpdate: Dispatcher).useEffectEvent = updateEvent; @@ -3957,6 +3959,7 @@ const HooksDispatcherOnRerender: Dispatcher = { useOptimistic: rerenderOptimistic, useMemoCache, useCacheRefresh: updateRefresh, + useSetStateWithCallback: rerenderSetStateWithCallback, }; if (enableUseEffectEventHook) { (HooksDispatcherOnRerender: Dispatcher).useEffectEvent = updateEvent; @@ -4150,6 +4153,7 @@ if (__DEV__) { mountHookTypesDev(); return mountRefresh(); }, + useSetStateWithCallback: mountSetStateWithCallback, }; if (enableUseEffectEventHook) { (HooksDispatcherOnMountInDEV: Dispatcher).useEffectEvent = @@ -4317,6 +4321,7 @@ if (__DEV__) { updateHookTypesDev(); return mountRefresh(); }, + useSetStateWithCallback: mountSetStateWithCallback, }; if (enableUseEffectEventHook) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEffectEvent = @@ -4484,6 +4489,7 @@ if (__DEV__) { updateHookTypesDev(); return updateRefresh(); }, + useSetStateWithCallback: updateSetStateWithCallback, }; if (enableUseEffectEventHook) { (HooksDispatcherOnUpdateInDEV: Dispatcher).useEffectEvent = @@ -4581,7 +4587,7 @@ if (__DEV__) { currentHookNameInDev = 'useState'; updateHookTypesDev(); const prevDispatcher = ReactSharedInternals.H; - ReactSharedInternals.H = InvalidNestedHooksDispatcherOnRerenderInDEV; + ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV; try { return rerenderState(initialState); } finally { @@ -4651,6 +4657,7 @@ if (__DEV__) { updateHookTypesDev(); return updateRefresh(); }, + useSetStateWithCallback: rerenderSetStateWithCallback, }; if (enableUseEffectEventHook) { (HooksDispatcherOnRerenderInDEV: Dispatcher).useEffectEvent = @@ -4842,6 +4849,7 @@ if (__DEV__) { mountHookTypesDev(); return mountRefresh(); }, + useSetStateWithCallback: mountSetStateWithCallback, }; if (enableUseEffectEventHook) { (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEffectEvent = @@ -5034,6 +5042,7 @@ if (__DEV__) { updateHookTypesDev(); return updateRefresh(); }, + useSetStateWithCallback: updateSetStateWithCallback, }; if (enableUseEffectEventHook) { (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEffectEvent = @@ -5226,6 +5235,7 @@ if (__DEV__) { updateHookTypesDev(); return updateRefresh(); }, + useSetStateWithCallback: rerenderSetStateWithCallback, }; if (enableUseEffectEventHook) { (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEffectEvent = @@ -5239,3 +5249,57 @@ if (__DEV__) { }; } } + +function mountSetStateWithCallback(initialValue: T): [T, (value: T, callback?: () => void) => void] { + const [state, setState] = mountState(initialValue); + const callbackRef = mountRef(null); + mountEffect(() => { + if (callbackRef.current) { + callbackRef.current(); + callbackRef.current = null; + } + }, [state]); + const setStateWithCallback = (value, callback) => { + if (callback) { + callbackRef.current = callback; + } + setState(value); + }; + return [state, setStateWithCallback]; +} + +function updateSetStateWithCallback(initialValue: T): [T, (value: T, callback?: () => void) => void] { + const [state, setState] = updateState(initialValue); + const callbackRef = updateRef(null); + updateEffect(() => { + if (callbackRef.current) { + callbackRef.current(); + callbackRef.current = null; + } + }, [state]); + const setStateWithCallback = (value, callback) => { + if (callback) { + callbackRef.current = callback; + } + setState(value); + }; + return [state, setStateWithCallback]; +} + +function rerenderSetStateWithCallback(initialValue: T): [T, (value: T, callback?: () => void) => void] { + const [state, setState] = rerenderState(initialValue); + const callbackRef = updateRef(null); + updateEffect(() => { + if (callbackRef.current) { + callbackRef.current(); + callbackRef.current = null; + } + }, [state]); + const setStateWithCallback = (value, callback) => { + if (callback) { + callbackRef.current = callback; + } + setState(value); + }; + return [state, setStateWithCallback]; +} diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index ec75513892900..abb63bdb9729a 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -62,7 +62,8 @@ export type HookType = | 'useCacheRefresh' | 'useOptimistic' | 'useFormState' - | 'useActionState'; + | 'useActionState' + | 'useSetStateWithCallback'; export type ContextDependency = { context: ReactContext, @@ -455,6 +456,7 @@ export type Dispatcher = { initialState: Awaited, permalink?: string, ) => [Awaited, (P) => void, boolean], + useSetStateWithCallback: (initialValue: T) => [T, (value: T, callback?: () => void) => void], }; export type AsyncDispatcher = { diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 119c2a0778fc6..a1c8d39d540b7 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -804,6 +804,10 @@ function clientHookNotSupported() { ); } +function unsupportedSetStateWithCallback() { + throw new Error('useSetStateWithCallback is not supported in Server Components.'); +} + export const HooksDispatcher: Dispatcher = supportsClientAPIs ? { readContext, @@ -833,6 +837,7 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs useHostTransitionStatus, useMemoCache, useCacheRefresh, + useSetStateWithCallback: unsupportedSetStateWithCallback, } : { readContext, @@ -858,6 +863,7 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs useOptimistic, useMemoCache, useCacheRefresh, + useSetStateWithCallback: unsupportedSetStateWithCallback, }; if (enableUseEffectEventHook) { diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js index ed369be0e9b18..a1421b5d464b8 100644 --- a/packages/react-server/src/ReactFlightHooks.js +++ b/packages/react-server/src/ReactFlightHooks.js @@ -101,6 +101,7 @@ export const HooksDispatcher: Dispatcher = { useCacheRefresh(): (?() => T, ?T) => void { return unsupportedRefresh; }, + useSetStateWithCallback: unsupportedSetStateWithCallback, }; if (enableUseEffectEventHook) { HooksDispatcher.useEffectEvent = (unsupportedHook: any); @@ -120,6 +121,10 @@ function unsupportedContext(): void { throw new Error('Cannot read a Client Context from a Server Component.'); } +function unsupportedSetStateWithCallback() { + throw new Error('useSetStateWithCallback is not supported in Server Components.'); +} + function useId(): string { if (currentRequest === null) { throw new Error('useId can only be used while React is rendering'); From 972e14708e684362f8282337e74e4e94b8c8c432 Mon Sep 17 00:00:00 2001 From: iamdivypatel Date: Fri, 18 Jul 2025 13:21:42 +0530 Subject: [PATCH 2/2] This commit introduces the useSetStateWithCallback hook across various React components, allowing state updates with an optional callback. The hook is implemented in the React reconciler and debug tools, with appropriate error handling for unsupported scenarios in server components. Additionally, the dispatcher types have been updated to include this new hook, ensuring compatibility with existing functionality. --- packages/react-debug-tools/src/ReactDebugHooks.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 9863e2bae044a..e94877de2ca55 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -1344,7 +1344,6 @@ export function inspectHooksOfFiber( currentFiber = null; currentHook = null; currentContextDependency = null; - restoreContexts(contextMap); } }