@samitouri / QOS-React / commits / e1c7e651fe

Update ReactDebugHooks to handle composite hooks (#18130)

The useState hook has always composed the useReducer hook. 1:1 composition like this is fine. But some more recent hooks (e.g. useTransition, useDeferredValue) compose multiple hooks internally. This breaks react-debug-tools because it causes off-by-N errors when the debug tools re-renders the function. For example, if a component were to use the useTransition and useMemo hooks, the normal hooks dispatcher would create a list of first state, then callback, then memo hooks, but the debug tools package would expect a list of transition then memo. This can break user code and cause runtime errors in both the react-debug-tools package and in product code. This PR fixes the currently broken hooks by updating debug tools to be aware of the composite hooks (how many times it should call nextHook essentially) and adds tests to make sure they don't get out of sync again. We'll need to add similar tests for future composite hooks (like useMutableSource #18000).

Brian Vaughn committed Feb 26, 2020 at 09:28 UTC e1c7e651feb7d8f0339db5720cfb61b036ee7290
2 files changed +68 -2
packages/react-debug-tools/src/ReactDebugHooks.js
+10 -2
@@ -243,7 +243,11 @@ function useResponder(
243 function useTransition(
244 config: SuspenseConfig | null | void,
245 ): [(() => void) => void, boolean] {
246 - nextHook();
246 + // useTransition() composes multiple hooks internally.
247 + // Advance the current hook index the same number of times
248 + // so that subsequent hooks have the right memoized state.
249 + nextHook(); // State
250 + nextHook(); // Callback
251 hookLog.push({
252 primitive: 'Transition',
253 stackError: new Error(),
@@ -253,7 +257,11 @@ function useTransition(
257 }
258
259 function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
256 - nextHook();
260 + // useDeferredValue() composes multiple hooks internally.
261 + // Advance the current hook index the same number of times
262 + // so that subsequent hooks have the right memoized state.
263 + nextHook(); // State
264 + nextHook(); // Effect
265 hookLog.push({
266 primitive: 'DeferredValue',
267 stackError: new Error(),
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+58
@@ -366,6 +366,64 @@ describe('ReactHooksInspectionIntegration', () => {
366 ]);
367 });
368
369 + if (__EXPERIMENTAL__) {
370 + it('should support composite useTransition hook', () => {
371 + function Foo(props) {
372 + React.useTransition();
373 + const memoizedValue = React.useMemo(() => 'hello', []);
374 + return <div>{memoizedValue}</div>;
375 + }
376 + let renderer = ReactTestRenderer.create(<Foo />);
377 + let childFiber = renderer.root.findByType(Foo)._currentFiber();
378 + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
379 + expect(tree).toEqual([
380 + {
381 + id: 0,
382 + isStateEditable: false,
383 + name: 'Transition',
384 + value: undefined,
385 + subHooks: [],
386 + },
387 + {
388 + id: 1,
389 + isStateEditable: false,
390 + name: 'Memo',
391 + value: 'hello',
392 + subHooks: [],
393 + },
394 + ]);
395 + });
396 +
397 + it('should support composite useDeferredValue hook', () => {
398 + function Foo(props) {
399 + React.useDeferredValue('abc', {
400 + timeoutMs: 500,
401 + });
402 + const [state] = React.useState(() => 'hello', []);
403 + return <div>{state}</div>;
404 + }
405 + let renderer = ReactTestRenderer.create(<Foo />);
406 + let childFiber = renderer.root.findByType(Foo)._currentFiber();
407 + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
408 + expect(tree).toEqual([
409 + {
410 + id: 0,
411 + isStateEditable: false,
412 + name: 'DeferredValue',
413 + value: 'abc',
414 + subHooks: [],
415 + },
416 + {
417 + id: 1,
418 + isStateEditable: true,
419 + name: 'State',
420 + value: 'hello',
421 + subHooks: [],
422 + },
423 + ]);
424 + });
425 + }
426 +
427 describe('useDebugValue', () => {
428 it('should support inspectable values for multiple custom hooks', () => {
429 function useLabeledValue(label) {