Skip to content

React dispatcher freeze mechanism#46

Draft
aidenybai wants to merge 5 commits intomainfrom
cursor/react-dispatcher-freeze-mechanism-652b
Draft

React dispatcher freeze mechanism#46
aidenybai wants to merge 5 commits intomainfrom
cursor/react-dispatcher-freeze-mechanism-652b

Conversation

@aidenybai
Copy link
Owner

Add freeze and isFreezeActive functions to block React state updates.

Initial attempts to freeze updates by patching dispatchers were insufficient because React components capture their dispatch functions during the initial render, making them referentially stable. The revised implementation intercepts updates at the queue.pending level within each hook's queue, preventing updates from being enqueued. It also patches dispatchers to ensure new components mounted while frozen also respect the freeze.


Open in Cursor Open in Web

Co-authored-by: aiden <aiden@million.dev>
@cursor
Copy link

cursor bot commented Jan 4, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@changeset-bot
Copy link

changeset-bot bot commented Jan 4, 2026

⚠️ No Changeset found

Latest commit: 9d5d487

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Jan 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
bippy Error Error Jan 17, 2026 7:57am

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 4, 2026

Open in StackBlitz

npm i https://pkg.pr.new/bippy@46

commit: 9d5d487

Co-authored-by: aiden <aiden@million.dev>
Co-authored-by: aiden <aiden@million.dev>
Rename freeze functionality to pause, update internal state names, and move tests to a new file.

Co-authored-by: aiden <aiden@million.dev>
@aidenybai
Copy link
Owner Author

@cursoragent how would i begin to support freezing:

Props changes from parent re-renders
Context changes
useTransition / useDeferredValue

@cursor
Copy link

cursor bot commented Jan 17, 2026

Now I understand the current freeze mechanism. Let me provide guidance on extending it to support props changes, context changes, and concurrent features.

Analysis of Extension Points

Based on the current implementation that patches hook queues and dispatchers, here's how to extend the freeze mechanism:

1. Props Changes from Parent Re-Renders

Props changes are the most challenging because they come from the reconciliation process itself. Here are the approaches:

Approach A: Patch the Reconciler's Work Loop

  • Access renderer.scheduleRefresh or internal scheduler functions
  • Intercept scheduleUpdateOnFiber to prevent scheduling updates
  • This requires accessing React internals through the renderer

Approach B: Patch beginWork Phase

  • During reconciliation, React calls beginWork for each fiber
  • We could patch fibers to skip the work phase when frozen
  • Set a flag on fibers or modify the lanes to prevent processing

Approach C: Bailout at Props Comparison

  • React compares old and new props to decide if a component should update
  • We could patch fiber.memoizedProps to make React think props haven't changed
  • This is hacky but might work

Here's a conceptual implementation:

const patchScheduler = (renderer: ReactRenderer): void => {
  // Access React's internal scheduler (if available)
  const scheduleUpdateOnFiber = renderer.scheduleUpdateOnFiber;
  
  if (scheduleUpdateOnFiber) {
    renderer.scheduleUpdateOnFiber = (fiber: Fiber, lane: number, eventTime: number) => {
      if (isUpdatesPaused) {
        // Skip scheduling this update
        return;
      }
      return scheduleUpdateOnFiber(fiber, lane, eventTime);
    };
  }
};

2. Context Changes

Context changes trigger updates through the fiber's dependencies field. When a context value changes, React walks all fibers with that context dependency.

Approach: Patch Context Propagation

const freezeContextDependencies = (fiber: Fiber): void => {
  if (!fiber.dependencies) return;
  
  // Store original context values
  const contextDependency = fiber.dependencies.firstContext;
  let current = contextDependency;
  
  while (current) {
    // Capture the current memoized value
    const frozenValue = current.memoizedValue;
    
    // Override to always return frozen value
    Object.defineProperty(current, 'memoizedValue', {
      get() { return frozenValue; },
      set() {}, // Ignore updates
      configurable: true
    });
    
    current = current.next;
  }
};

3. useTransition / useDeferredValue

These concurrent features use internal state. They work through hooks that schedule transitions.

Approach: Patch Transition Hooks in Dispatcher

const createFrozenDispatcher = (originalDispatcher: Dispatcher): Dispatcher => {
  return new Proxy(originalDispatcher, {
    get(target, prop, receiver) {
      const originalMethod = Reflect.get(target, prop, receiver);

      if (prop === 'useTransition') {
        return (): [boolean, (callback: () => void) => void] => {
          const [isPending, startTransition] = originalMethod();
          
          // When frozen, make startTransition a no-op
          const frozenStartTransition = (callback: () => void) => {
            if (isUpdatesPaused) return;
            startTransition(callback);
          };
          
          return [isPending, frozenStartTransition];
        };
      }

      if (prop === 'useDeferredValue') {
        return <T>(value: T): T => {
          // When frozen, return the value from when freeze started
          if (isUpdatesPaused) {
            // Need to capture the value at freeze time
            return getFrozenDeferredValue(value);
          }
          return originalMethod(value);
        };
      }

      return originalMethod;
    },
  });
};

Recommended Implementation Strategy

  1. Start with Context - This is the most straightforward extension
  2. Add Transition/Deferred - Extend the dispatcher proxy that already exists
  3. Tackle Props Last - This requires deeper React internals access

Would you like me to implement these extensions? I can start with a working implementation for context changes and concurrent features, which are more achievable without deep React internals patching.

Open in Cursor Open in Web

@aidenybai
Copy link
Owner Author

@cursoragent any of this useful?

User:

i implemented freezing for setState by patching the dispatcher

how would i begin to support freezing:

Props changes from parent re-renders
Context changes
useTransition / useDeferredValue


Assistant:

I'll search for your setState freezing implementation to understand the dispatcher patch approach, then show how to extend it.

Let me search more broadly for freezing implementations in this React codebase:

It seems the freezing implementation isn't in the main React repo yet. Let me provide a practical approach based on React's architecture. Here's how to extend dispatcher freezing to cover the three mechanisms you mentioned:

Strategy

Your current dispatcher patch works for setState by intercepting updates. To extend it, you need to patch at the entry points where these three mechanisms trigger renders:

1. Props Changes from Parent Re-renders

Patch the reconciliation process that applies new props:

// In React's FiberScheduler or your custom hook wrapper
const originalReconcile = fiber.reconcile;

fiber.reconcile = function(newProps) {
  // Freeze before props are applied to children
  freezeState('props-change');
  
  return originalReconcile.call(this, newProps);
};

Or more practically, intercept prop updates in your custom hook:

function useFreezeOnPropChange(props) {
  const propsRef = useRef(props);
  
  useEffect(() => {
    // Detect prop change
    if (JSON.stringify(props) !== JSON.stringify(propsRef.current)) {
      freezeState('props-changed');
      propsRef.current = props;
    }
  }, [props]);
}

2. Context Changes

Patch context subscription in the same way. React's useContext hook reads from Context.Consumer internals. Wrap it:

const originalUseContext = React.useContext;

React.useContext = function(context) {
  // Get the context value
  const value = originalUseContext.call(this, context);
  
  // Track if value changed
  const contextRef = useRef(value);
  useEffect(() => {
    if (value !== contextRef.current) {
      freezeState('context-change');
      contextRef.current = value;
    }
  }, [value]);
  
  return value;
};

Or simpler with a custom hook:

function useFreezeOnContextChange(context) {
  const value = useContext(context);
  const prevValue = useRef(value);
  
  useEffect(() => {
    if (value !== prevValue.current) {
      freezeState('context-changed');
      prevValue.current = value;
    }
  }, [value]);
  
  return value;
}

3. useTransition / useDeferredValue

These already batch updates, but you can hook into their render phase:

const originalUseTransition = React.useTransition;

React.useTransition = function() {
  const [isPending, startTransition] = originalUseTransition.call(this);
  
  const wrappedStartTransition = useCallback((callback) => {
    freezeState('transition-start');
    startTransition(() => {
      callback();
      freezeState('transition-end');
    });
  }, [startTransition]);
  
  return [isPending, wrappedStartTransition];
};

Unified Approach: Patch Render Scheduling

Instead of patching each entry point separately, patch React's scheduler where all renders converge:

let currentFreezeReason = null;

// Patch Scheduler.scheduleCallback (or useInsertionEffect-like timing)
const originalSchedule = Scheduler.scheduleCallback;

Scheduler.scheduleCallback = function(priority, callback) {
  const wrappedCallback = () => {
    if (currentFreezeReason) {
      freezeState(currentFreezeReason);
    }
    callback();
    unfreezeState();
  };
  
  return originalSchedule.call(this, priority, wrappedCallback);
};

// Then in each trigger point:
export function withFreezeReason(reason, fn) {
  const prev = currentFreezeReason;
  currentFreezeReason = reason;
  try {
    fn();
  } finally {
    currentFreezeReason = prev;
  }
}

// Usage:
withFreezeReason('props-changed', () => {
  updateProps(newProps);
});

This approach is cleaner because all three mechanisms ultimately call the scheduler, so a single patch covers them all.


User:

im on the outside (we can only access __REACT_DEVTOLS_GLOBAL_HOOK


Assistant:

That changes everything—you're working from DevTools. Here's how to hook freezing into those three mechanisms via __REACT_DEVTOOLS_GLOBAL_HOOK__:

Props Changes & Context Changes

Hook into the fiber update cycle via the DevTools hook's commit phase:

const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;

const originalCommitFiberRoot = hook.onCommitFiberRoot;

hook.onCommitFiberRoot = function(id, root, priorityLevel) {
  const fiber = root.current;
  const prevProps = fiber._prevProps;
  const prevContext = fiber._prevContextValue;
  
  // Check for prop changes
  if (prevProps && prevProps !== fiber.memoizedProps) {
    freezeState('props-changed');
  }
  
  // Check for context changes
  if (prevContext && prevContext !== fiber.memoizedState) {
    freezeState('context-changed');
  }
  
  return originalCommitFiberRoot.call(this, id, root, priorityLevel);
};

useTransition / useDeferredValue

These leave traces in the fiber's lane priority. Hook into render scheduling:

const originalOnCommitFiberRoot = hook.onCommitFiberRoot;

hook.onCommitFiberRoot = function(id, root, priorityLevel) {
  const fiber = root.current;
  
  // Check if this is a deferred/transition update
  if (root.pendingLanes !== 0) {
    const hasTransitionLane = (root.pendingLanes & 0x10000) !== 0; // Transition lane
    const hasDeferredLane = (root.pendingLanes & 0x100000) !== 0;  // Deferred lane
    
    if (hasTransitionLane || hasDeferredLane) {
      freezeState('deferred-update');
    }
  }
  
  return originalOnCommitFiberRoot.call(this, id, root, priorityLevel);
};

All-in-One: Intercept setState Dispatcher at DevTools Level

Since you already patch the dispatcher, do it via DevTools hooks for broader coverage:

const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;

// Track dispatcher operations
const originalOnCommitFiberRoot = hook.onCommitFiberRoot;

hook.onCommitFiberRoot = function(id, root, priorityLevel) {
  const fiber = root.current;
  const updateQueue = fiber.updateQueue;
  
  if (updateQueue) {
    const lastUpdate = updateQueue.lastUpdate;
    
    // Detect update type
    if (lastUpdate?.tag === 'state') {
      freezeState('setState');
    } else if (lastUpdate?.tag === 'context') {
      freezeState('context-change');
    } else if (lastUpdate?.tag === 'deferred') {
      freezeState('deferred-value');
    }
  }
  
  return originalOnCommitFiberRoot.call(this, id, root, priorityLevel);
};

Practical Implementation

Store a freeze registry keyed by fiber identity:

const freezeRegistry = new WeakMap();

hook.onCommitFiberRoot = function(id, root, priorityLevel) {
  let fiber = root.current;
  
  while (fiber) {
    const state = fiber.memoizedState;
    
    // Check if props, context, or deferred updates changed
    if (fiberChanged(fiber)) {
      freezeRegistry.set(fiber, { 
        reason: detectChangeReason(fiber),
        frozenAt: performance.now()
      });
    }
    
    fiber = fiber.return;
  }
  
  return originalOnCommitFiberRoot.call(this, id, root, priorityLevel);
};

// When setState fires, check if component should be frozen
function shouldFreeze(fiber) {
  return freezeRegistry.has(fiber);
}

The key insight: DevTools hooks see the fiber tree after React's reconciliation, so you can inspect fiber.lanes, fiber.updateQueue, and sibling props to determine which mechanism triggered the render.

@cursor
Copy link

cursor bot commented Jan 17, 2026

Looking at your current implementation, some of the external response is on the right track but not quite aligned with your DevTools-based architecture. Let me provide more practical guidance:

What's Actually Useful

The external response correctly identifies that you need to work through __REACT_DEVTOOLS_GLOBAL_HOOK__, but the specific approaches need refinement based on your existing implementation.

Concrete Extensions to Your Current Code

1. Context Changes (Most Achievable)

Extend your existing fiber traversal to freeze context dependencies:

const pauseContextDependencies = (fiber: Fiber): void => {
  if (!fiber.dependencies) return;
  
  let contextDependency = fiber.dependencies.firstContext as ContextDependency<unknown> | null;
  const frozenContexts = new WeakMap<ContextDependency<unknown>, unknown>();
  
  while (contextDependency) {
    frozenContexts.set(contextDependency, contextDependency.memoizedValue);
    
    const originalDescriptor = Object.getOwnPropertyDescriptor(contextDependency, 'memoizedValue');
    
    Object.defineProperty(contextDependency, 'memoizedValue', {
      configurable: true,
      enumerable: true,
      get() {
        if (isUpdatesPaused) {
          return frozenContexts.get(contextDependency);
        }
        return originalDescriptor?.get?.call(this) ?? this._memoizedValue;
      },
      set(newValue) {
        if (isUpdatesPaused) return;
        if (originalDescriptor?.set) {
          originalDescriptor.set.call(this, newValue);
        } else {
          this._memoizedValue = newValue;
        }
      },
    });
    
    contextDependency = contextDependency.next;
  }
};

Add this to your traverseAndPauseHookQueues function.

2. useTransition / useDeferredValue (Extend Existing Dispatcher Proxy)

Your createPausedDispatcher already handles hooks. Extend it:

if (prop === 'useTransition') {
  return (): [boolean, (callback: () => void) => void] => {
    const result = originalMethod();
    if (!Array.isArray(result)) return result;
    
    const [isPending, startTransition] = result;
    const frozenStartTransition = (callback: () => void) => {
      if (isUpdatesPaused) return;
      startTransition(callback);
    };
    
    return [isPending, frozenStartTransition];
  };
}

if (prop === 'useDeferredValue') {
  return <T>(value: T): T => {
    const deferredValue = originalMethod(value);
    // When frozen, the deferred value won't update because
    // the underlying state updates are already frozen
    return deferredValue;
  };
}

3. Props Changes (Most Complex)

Props changes require preventing reconciliation work. The external response's suggestion to check fiber.lanes is actually useful here. You could patch the renderer's scheduling:

const patchRendererScheduling = (renderer: ReactRenderer): void => {
  // React's internal scheduleUpdateOnFiber (if accessible)
  const scheduleUpdate = (renderer as any).scheduleUpdateOnFiber;
  
  if (typeof scheduleUpdate === 'function') {
    (renderer as any).scheduleUpdateOnFiber = function(fiber: Fiber, lane: number, eventTime: number) {
      if (isUpdatesPaused) {
        // Don't schedule the update
        return;
      }
      return scheduleUpdate.call(this, fiber, lane, eventTime);
    };
  }
};

However, this is fragile because scheduleUpdateOnFiber is not guaranteed to be exposed.

The Most Reliable Approach

For props, the reality is that preventing parent re-renders from affecting children while staying outside React's internals is extremely difficult. The most reliable approach might be to accept that props will update but ensure the resulting state changes don't propagate.

Your current implementation already does this by freezing hook queues - even if a component re-renders with new props, any state updates it triggers will be blocked.

Bottom Line

  • Context freezing: Definitely achievable and useful
  • Transition/Deferred hooks: Easy extension to existing code
  • Props from parent renders: Extremely difficult without React internals access

The external response's fiber inspection ideas are interesting but not directly actionable with your DevTools-only access constraint.

Open in Cursor Open in Web

@aidenybai
Copy link
Owner Author

can we hack it with overrideProps?

@aidenybai
Copy link
Owner Author

@cursoragent can we hack it with overrideProps?

@cursor
Copy link

cursor bot commented Jan 17, 2026

Interesting idea! Using overrideProps is creative, but unfortunately it won't work directly for freezing props. Here's why:

The Problem with overrideProps

Looking at your existing implementation, overrideProps is designed to trigger updates, not prevent them. It works by:

  1. Calling renderer.overrideProps(fiber, path, value)
  2. This schedules a new render with modified props
  3. React reconciles and re-renders the component

So it's the opposite of what we need - it causes renders rather than preventing them.

A Potential Hack

However, there's a different approach inspired by how overrideProps works: directly patching fiber props.

const frozenPropsMap = new WeakMap<Fiber, unknown>();

const freezeFiberProps = (fiber: Fiber): void => {
  if (!isCompositeFiber(fiber)) return;
  
  frozenPropsMap.set(fiber, fiber.memoizedProps);
  
  const originalPropsDescriptor = Object.getOwnPropertyDescriptor(fiber, 'memoizedProps');
  
  Object.defineProperty(fiber, 'memoizedProps', {
    configurable: true,
    enumerable: true,
    get() {
      if (isUpdatesPaused) {
        return frozenPropsMap.get(fiber);
      }
      return originalPropsDescriptor?.get?.call(this) ?? this._memoizedProps;
    },
    set(newProps) {
      if (isUpdatesPaused) return;
      if (originalPropsDescriptor?.set) {
        originalPropsDescriptor.set.call(this, newProps);
      } else {
        this._memoizedProps = newProps;
      }
    },
  });
  
  // Also freeze pendingProps
  Object.defineProperty(fiber, 'pendingProps', {
    configurable: true,
    get() {
      if (isUpdatesPaused) {
        return frozenPropsMap.get(fiber);
      }
      return this._pendingProps;
    },
    set(newProps) {
      if (!isUpdatesPaused) {
        this._pendingProps = newProps;
      }
    },
  });
};

This directly intercepts prop assignment on the fiber, similar to how you're already freezing hook queues.

The Catch

This approach has significant caveats:

  • React's double buffering: Fibers have alternates, so you'd need to patch both
  • New fibers: Components that mount while frozen won't have patched props
  • Reconciliation bailout: React might still run the reconciliation logic even if props appear unchanged

It's theoretically possible but much more fragile than the hook queue approach. The hook queue freezing works well because it intercepts at the update queue level. Props flow through reconciliation, which is harder to intercept cleanly from the outside.

Worth experimenting with, but I'd set expectations that it may not be robust enough for production use.

Open in Cursor Open in Web

@aidenybai
Copy link
Owner Author

nvm just do the context transitin and deferred values. do feat detection tho

@aidenybai
Copy link
Owner Author

@cursoragent nvm just do the context transitin and deferred values. do feat detection tho

@cursor
Copy link

cursor bot commented Jan 17, 2026

I've successfully implemented freezing support for context, useTransition, and useDeferredValue with proper feature detection. Here's what was added:

Changes Made

1. Context Dependency Freezing

  • Added pauseContextDependency and resumeContextDependency functions to freeze context values
  • When frozen, context changes won't propagate to components consuming that context
  • Integrated into the existing fiber traversal logic

2. useTransition Support

  • Extended the dispatcher proxy to intercept useTransition calls
  • When frozen, startTransition callbacks become no-ops
  • Includes feature detection to gracefully handle React versions without this hook

3. useDeferredValue Support

  • Added dispatcher proxy handling for useDeferredValue
  • Works automatically since the underlying state updates are already frozen
  • Includes feature detection for compatibility

4. Comprehensive Tests

  • Added test for context updates being paused
  • Added test for useTransition being blocked when frozen
  • Added test for useDeferredValue handling
  • All 90 tests pass, including the 3 new ones

Key Features

  • Feature detection: All new hooks check if they exist before patching
  • Type safety: Proper TypeScript interfaces and type guards
  • Error handling: Try-catch blocks around context traversal
  • Documentation: Updated JSDoc to reflect new capabilities

The implementation follows the same pattern as the existing hook queue freezing - using property descriptors to intercept reads/writes at the React internals level, ensuring compatibility across React versions.

Open in Cursor Open in Web

- Add context dependency freezing to prevent context changes from triggering updates
- Extend dispatcher proxy to handle useTransition and useDeferredValue hooks
- Add feature detection for all new freeze mechanisms
- Include comprehensive tests for context, useTransition, and useDeferredValue
- Update documentation to reflect new capabilities

Co-authored-by: aiden <aiden@million.dev>
@aidenybai
Copy link
Owner Author

@cursoragent write some retsts for this and test it

@cursor
Copy link

cursor bot commented Jan 17, 2026

Perfect! The tests are already written and passing. Here's a summary of what I added:

Tests Written

1. Context Updates Test (Lines 218-263)

Tests that context value changes are properly frozen:

  • Sets up a Context Provider with a consumer component
  • Updates context value normally (works)
  • Freezes updates with pauseUpdates()
  • Attempts to update context while frozen (blocked)
  • Resumes and verifies updates work again

2. useTransition Test (Lines 265-313)

Tests that startTransition calls are blocked when frozen:

  • Component uses useTransition hook
  • Updates work normally via startTransition
  • Freezes updates
  • Attempts transition update while frozen (blocked)
  • Resumes and verifies transitions work again

3. useDeferredValue Test (Lines 315-348)

Tests that deferred values handle freezing gracefully:

  • Component uses useDeferredValue hook
  • Updates deferred value normally
  • Freezes and attempts update (blocked by underlying state freeze)
  • Demonstrates the hook doesn't break when frozen

Test Results

All 90 tests passing including:

  • 3 new tests for context/transition/deferred freezing
  • 7 existing tests for useState/useReducer/useSyncExternalStore

The implementation includes proper feature detection - the dispatcher proxy checks if hooks exist before wrapping them, ensuring compatibility across React versions.

Would you like me to add any additional edge case tests or test different scenarios?

Open in Cursor Open in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants