Conversation
Co-authored-by: aiden <aiden@million.dev>
|
Cursor Agent can help with this pull request. Just |
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
commit: |
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>
|
@cursoragent how would i begin to support freezing: Props changes from parent re-renders |
|
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 PointsBased on the current implementation that patches hook queues and dispatchers, here's how to extend the freeze mechanism: 1. Props Changes from Parent Re-RendersProps 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
Approach B: Patch
Approach C: Bailout at Props Comparison
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 ChangesContext changes trigger updates through the fiber's 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 / useDeferredValueThese 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
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. |
|
@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 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: StrategyYour current dispatcher patch works for 1. Props Changes from Parent Re-rendersPatch 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 ChangesPatch context subscription in the same way. React's 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 / useDeferredValueThese 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 SchedulingInstead 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 Props Changes & Context ChangesHook 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 / useDeferredValueThese 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 LevelSince 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 ImplementationStore 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 |
|
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 UsefulThe external response correctly identifies that you need to work through Concrete Extensions to Your Current Code1. 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 2. useTransition / useDeferredValue (Extend Existing Dispatcher Proxy)Your 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 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 The Most Reliable ApproachFor 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
The external response's fiber inspection ideas are interesting but not directly actionable with your DevTools-only access constraint. |
|
can we hack it with overrideProps? |
|
@cursoragent can we hack it with overrideProps? |
|
Interesting idea! Using The Problem with
|
|
nvm just do the context transitin and deferred values. do feat detection tho |
|
@cursoragent nvm just do the context transitin and deferred values. do feat detection tho |
|
I've successfully implemented freezing support for context, useTransition, and useDeferredValue with proper feature detection. Here's what was added: Changes Made1. Context Dependency Freezing
2. useTransition Support
3. useDeferredValue Support
4. Comprehensive Tests
Key Features
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. |
- 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>
|
@cursoragent write some retsts for this and test it |
|
Perfect! The tests are already written and passing. Here's a summary of what I added: Tests Written1. Context Updates Test (Lines 218-263)Tests that context value changes are properly frozen:
2. useTransition Test (Lines 265-313)Tests that
3. useDeferredValue Test (Lines 315-348)Tests that deferred values handle freezing gracefully:
Test Results✅ All 90 tests passing including:
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? |
Add
freezeandisFreezeActivefunctions 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.pendinglevel 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.