From a8604c972ca80de15522d324e0704a03038bdee8 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 6 Jul 2025 10:45:26 +0200 Subject: [PATCH 1/3] Revert "Look at impact of removing deprecated lifecycles (#4656)" This reverts commit 4e74070baa087fd2b7a412ba21a5d61b8c70a0a8. --- compat/src/render.js | 27 +++ compat/test/browser/component.test.js | 250 ++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) diff --git a/compat/src/render.js b/compat/src/render.js index 9807c0fc0b..c11c4e4773 100644 --- a/compat/src/render.js +++ b/compat/src/render.js @@ -40,6 +40,33 @@ const onChangeInputType = type => /fil|che|rad/.test(type); // Some libraries like `react-virtualized` explicitly check for this. Component.prototype.isReactComponent = {}; +// `UNSAFE_*` lifecycle hooks +// Preact only ever invokes the unprefixed methods. +// Here we provide a base "fallback" implementation that calls any defined UNSAFE_ prefixed method. +// - If a component defines its own `componentDidMount()` (including via defineProperty), use that. +// - If a component defines `UNSAFE_componentDidMount()`, `componentDidMount` is the alias getter/setter. +// - If anything assigns to an `UNSAFE_*` property, the assignment is forwarded to the unprefixed property. +// See https://github.com/preactjs/preact/issues/1941 +[ + 'componentWillMount', + 'componentWillReceiveProps', + 'componentWillUpdate' +].forEach(key => { + Object.defineProperty(Component.prototype, key, { + configurable: true, + get() { + return this['UNSAFE_' + key]; + }, + set(v) { + Object.defineProperty(this, key, { + configurable: true, + writable: true, + value: v + }); + } + }); +}); + /** * Proxy render() since React returns a Component reference. * @param {import('./internal').VNode} vnode VNode tree to render diff --git a/compat/test/browser/component.test.js b/compat/test/browser/component.test.js index 9aecf754bc..8ccf71c69d 100644 --- a/compat/test/browser/component.test.js +++ b/compat/test/browser/component.test.js @@ -75,4 +75,254 @@ describe('components', () => { children: 'second' }); }); + + describe('UNSAFE_* lifecycle methods', () => { + it('should support UNSAFE_componentWillMount', () => { + let spy = sinon.spy(); + + class Foo extends React.Component { + // eslint-disable-next-line camelcase + UNSAFE_componentWillMount() { + spy(); + } + + render() { + return

foo

; + } + } + + React.render(, scratch); + + expect(spy).to.be.calledOnce; + }); + + it('should support UNSAFE_componentWillMount #2', () => { + let spy = sinon.spy(); + + class Foo extends React.Component { + render() { + return

foo

; + } + } + + Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillMount', { + value: spy + }); + + React.render(, scratch); + expect(spy).to.be.calledOnce; + }); + + it('should support UNSAFE_componentWillReceiveProps', () => { + let spy = sinon.spy(); + + class Foo extends React.Component { + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps() { + spy(); + } + + render() { + return

foo

; + } + } + + React.render(, scratch); + // Trigger an update + React.render(, scratch); + expect(spy).to.be.calledOnce; + }); + + it('should support UNSAFE_componentWillReceiveProps #2', () => { + let spy = sinon.spy(); + + class Foo extends React.Component { + render() { + return

foo

; + } + } + + Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillReceiveProps', { + value: spy + }); + + React.render(, scratch); + // Trigger an update + React.render(, scratch); + expect(spy).to.be.calledOnce; + }); + + it('should support UNSAFE_componentWillUpdate', () => { + let spy = sinon.spy(); + + class Foo extends React.Component { + // eslint-disable-next-line camelcase + UNSAFE_componentWillUpdate() { + spy(); + } + + render() { + return

foo

; + } + } + + React.render(, scratch); + // Trigger an update + React.render(, scratch); + expect(spy).to.be.calledOnce; + }); + + it('should support UNSAFE_componentWillUpdate #2', () => { + let spy = sinon.spy(); + + class Foo extends React.Component { + render() { + return

foo

; + } + } + + Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillUpdate', { + value: spy + }); + + React.render(, scratch); + // Trigger an update + React.render(, scratch); + expect(spy).to.be.calledOnce; + }); + + it('should alias UNSAFE_* method to non-prefixed variant', () => { + let inst; + class Foo extends React.Component { + // eslint-disable-next-line camelcase + UNSAFE_componentWillMount() {} + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps() {} + // eslint-disable-next-line camelcase + UNSAFE_componentWillUpdate() {} + render() { + inst = this; + return
foo
; + } + } + + React.render(, scratch); + + expect(inst.UNSAFE_componentWillMount).to.equal(inst.componentWillMount); + expect(inst.UNSAFE_componentWillReceiveProps).to.equal( + inst.UNSAFE_componentWillReceiveProps + ); + expect(inst.UNSAFE_componentWillUpdate).to.equal( + inst.UNSAFE_componentWillUpdate + ); + }); + + it('should call UNSAFE_* methods through Suspense with wrapper component #2525', () => { + class Page extends React.Component { + UNSAFE_componentWillMount() {} + render() { + return

Example

; + } + } + + const Wrapper = () => ; + + sinon.spy(Page.prototype, 'UNSAFE_componentWillMount'); + + React.render( + fallback}> + + , + scratch + ); + + expect(scratch.innerHTML).to.equal('

Example

'); + expect(Page.prototype.UNSAFE_componentWillMount).to.have.been.called; + }); + }); + + describe('defaultProps', () => { + it('should apply default props on initial render', () => { + class WithDefaultProps extends Component { + constructor(props, context) { + super(props, context); + expect(props).to.be.deep.equal({ + fieldA: 1, + fieldB: 2, + fieldC: 1, + fieldD: 2 + }); + } + render() { + return
; + } + } + WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; + React.render( + , + scratch + ); + }); + + it('should apply default props on rerender', () => { + let doRender; + class Outer extends Component { + constructor() { + super(); + this.state = { i: 1 }; + } + componentDidMount() { + doRender = () => this.setState({ i: 2 }); + } + render(props, { i }) { + return ; + } + } + class WithDefaultProps extends Component { + constructor(props, context) { + super(props, context); + this.ctor(props, context); + } + ctor() {} + componentWillReceiveProps() {} + render() { + return
; + } + } + WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; + + let proto = WithDefaultProps.prototype; + sinon.spy(proto, 'ctor'); + sinon.spy(proto, 'componentWillReceiveProps'); + sinon.spy(proto, 'render'); + + React.render(, scratch); + doRender(); + + const PROPS1 = { + fieldA: 1, + fieldB: 1, + fieldC: 1, + fieldD: 1 + }; + + const PROPS2 = { + fieldA: 1, + fieldB: 2, + fieldC: 1, + fieldD: 2 + }; + + expect(proto.ctor).to.have.been.calledWithMatch(PROPS1); + expect(proto.render).to.have.been.calledWithMatch(PROPS1); + + rerender(); + + // expect(proto.ctor).to.have.been.calledWith(PROPS2); + expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch( + PROPS2 + ); + expect(proto.render).to.have.been.calledWithMatch(PROPS2); + }); + }); }); From 6155178db6367a538e4a9cf8e88a4a2fac2ac616 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 6 Jul 2025 10:45:47 +0200 Subject: [PATCH 2/3] Revert "Fix mistake in sCU this binding" This reverts commit b3e8fa0757141a1d75decdad06860fcfaac0558a. --- hooks/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/src/index.js b/hooks/src/index.js index eb52ae8e51..86b407f302 100644 --- a/hooks/src/index.js +++ b/hooks/src/index.js @@ -201,7 +201,7 @@ export function useReducer(reducer, initialState, init) { currentComponent._hasScuFromHooks = true; let prevScu = currentComponent.shouldComponentUpdate; - currentComponent.shouldComponentUpdate = function (p, s, c) { + currentComponent.shouldComponentUpdate = (p, s, c) => { return prevScu ? prevScu.call(this, p, s, c) || hookState._actions.length : hookState._actions.length; From 460d316d8e684c01edbbdee0d5459bcafb22f4dc Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 6 Jul 2025 10:55:13 +0200 Subject: [PATCH 3/3] Revert "Implement deferring state updates (#4760)" This reverts commit d4d41f28fe26d995aef859b3a5a50ed1aaf114fc. --- compat/test/browser/component.test.js | 47 ++++---- hooks/src/index.js | 123 ++++++++++++-------- hooks/src/internal.d.ts | 7 +- hooks/test/browser/useState.test.js | 160 ++------------------------ mangle.json | 4 +- src/constants.js | 2 - src/diff/index.js | 15 --- src/internal.d.ts | 2 - 8 files changed, 111 insertions(+), 249 deletions(-) diff --git a/compat/test/browser/component.test.js b/compat/test/browser/component.test.js index 8ccf71c69d..5abe301cc0 100644 --- a/compat/test/browser/component.test.js +++ b/compat/test/browser/component.test.js @@ -1,6 +1,7 @@ import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown } from '../../../test/_util/helpers'; -import React, { createElement } from 'preact/compat'; +import React, { createElement, Component } from 'preact/compat'; +import { vi } from 'vitest'; describe('components', () => { /** @type {HTMLDivElement} */ @@ -78,7 +79,7 @@ describe('components', () => { describe('UNSAFE_* lifecycle methods', () => { it('should support UNSAFE_componentWillMount', () => { - let spy = sinon.spy(); + let spy = vi.fn(); class Foo extends React.Component { // eslint-disable-next-line camelcase @@ -93,11 +94,11 @@ describe('components', () => { React.render(, scratch); - expect(spy).to.be.calledOnce; + expect(spy).toHaveBeenCalledOnce(); }); it('should support UNSAFE_componentWillMount #2', () => { - let spy = sinon.spy(); + let spy = vi.fn(); class Foo extends React.Component { render() { @@ -110,11 +111,11 @@ describe('components', () => { }); React.render(, scratch); - expect(spy).to.be.calledOnce; + expect(spy).toHaveBeenCalledOnce(); }); it('should support UNSAFE_componentWillReceiveProps', () => { - let spy = sinon.spy(); + let spy = vi.fn(); class Foo extends React.Component { // eslint-disable-next-line camelcase @@ -130,11 +131,11 @@ describe('components', () => { React.render(, scratch); // Trigger an update React.render(, scratch); - expect(spy).to.be.calledOnce; + expect(spy).toHaveBeenCalledOnce(); }); it('should support UNSAFE_componentWillReceiveProps #2', () => { - let spy = sinon.spy(); + let spy = vi.fn(); class Foo extends React.Component { render() { @@ -149,11 +150,11 @@ describe('components', () => { React.render(, scratch); // Trigger an update React.render(, scratch); - expect(spy).to.be.calledOnce; + expect(spy).toHaveBeenCalledOnce(); }); it('should support UNSAFE_componentWillUpdate', () => { - let spy = sinon.spy(); + let spy = vi.fn(); class Foo extends React.Component { // eslint-disable-next-line camelcase @@ -169,11 +170,11 @@ describe('components', () => { React.render(, scratch); // Trigger an update React.render(, scratch); - expect(spy).to.be.calledOnce; + expect(spy).toHaveBeenCalledOnce(); }); it('should support UNSAFE_componentWillUpdate #2', () => { - let spy = sinon.spy(); + let spy = vi.fn(); class Foo extends React.Component { render() { @@ -188,7 +189,7 @@ describe('components', () => { React.render(, scratch); // Trigger an update React.render(, scratch); - expect(spy).to.be.calledOnce; + expect(spy).toHaveBeenCalledOnce(); }); it('should alias UNSAFE_* method to non-prefixed variant', () => { @@ -227,7 +228,7 @@ describe('components', () => { const Wrapper = () => ; - sinon.spy(Page.prototype, 'UNSAFE_componentWillMount'); + vi.spyOn(Page.prototype, 'UNSAFE_componentWillMount'); React.render( fallback
}> @@ -237,7 +238,7 @@ describe('components', () => { ); expect(scratch.innerHTML).to.equal('

Example

'); - expect(Page.prototype.UNSAFE_componentWillMount).to.have.been.called; + expect(Page.prototype.UNSAFE_componentWillMount).toHaveBeenCalled(); }); }); @@ -292,9 +293,9 @@ describe('components', () => { WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; let proto = WithDefaultProps.prototype; - sinon.spy(proto, 'ctor'); - sinon.spy(proto, 'componentWillReceiveProps'); - sinon.spy(proto, 'render'); + vi.spyOn(proto, 'ctor'); + vi.spyOn(proto, 'componentWillReceiveProps'); + vi.spyOn(proto, 'render'); React.render(, scratch); doRender(); @@ -313,16 +314,14 @@ describe('components', () => { fieldD: 2 }; - expect(proto.ctor).to.have.been.calledWithMatch(PROPS1); - expect(proto.render).to.have.been.calledWithMatch(PROPS1); + expect(proto.ctor).toHaveBeenCalledWith(PROPS1, {}); + expect(proto.render).toHaveBeenCalledWith(PROPS1, {}, {}); rerender(); // expect(proto.ctor).to.have.been.calledWith(PROPS2); - expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch( - PROPS2 - ); - expect(proto.render).to.have.been.calledWithMatch(PROPS2); + expect(proto.componentWillReceiveProps).toHaveBeenCalledWith(PROPS2, {}); + expect(proto.render).toHaveBeenCalledWith(PROPS2, {}, {}); }); }); }); diff --git a/hooks/src/index.js b/hooks/src/index.js index 86b407f302..d6e66dfe7f 100644 --- a/hooks/src/index.js +++ b/hooks/src/index.js @@ -1,5 +1,4 @@ import { options as _options } from 'preact'; -import { SKIP_CHILDREN } from '../../src/constants'; const ObjectIs = Object.is; @@ -27,7 +26,6 @@ let oldAfterDiff = options.diffed; let oldCommit = options._commit; let oldBeforeUnmount = options.unmount; let oldRoot = options._root; -let oldAfterRender = options._afterRender; // We take the minimum timeout for requestAnimationFrame to ensure that // the callback is invoked after the next frame. 35ms is based on a 30hz @@ -62,7 +60,10 @@ options._render = vnode => { hooks._pendingEffects = []; currentComponent._renderCallbacks = []; hooks._list.forEach(hookItem => { - hookItem._pendingArgs = undefined; + if (hookItem._nextValue) { + hookItem._value = hookItem._nextValue; + } + hookItem._pendingArgs = hookItem._nextValue = undefined; }); } else { hooks._pendingEffects.forEach(invokeCleanup); @@ -185,13 +186,19 @@ export function useReducer(reducer, initialState, init) { const hookState = getHookState(currentIndex++, 2); hookState._reducer = reducer; if (!hookState._component) { - hookState._actions = []; hookState._value = [ !init ? invokeOrReturn(undefined, initialState) : init(initialState), action => { - hookState._actions.push(action); - hookState._component.setState({}); + const currentValue = hookState._nextValue + ? hookState._nextValue[0] + : hookState._value[0]; + const nextValue = hookState._reducer(currentValue, action); + + if (!ObjectIs(currentValue, nextValue)) { + hookState._nextValue = [nextValue, hookState._value[1]]; + hookState._component.setState({}); + } } ]; @@ -200,55 +207,75 @@ export function useReducer(reducer, initialState, init) { if (!currentComponent._hasScuFromHooks) { currentComponent._hasScuFromHooks = true; let prevScu = currentComponent.shouldComponentUpdate; - - currentComponent.shouldComponentUpdate = (p, s, c) => { - return prevScu - ? prevScu.call(this, p, s, c) || hookState._actions.length - : hookState._actions.length; + const prevCWU = currentComponent.componentWillUpdate; + + // If we're dealing with a forced update `shouldComponentUpdate` will + // not be called. But we use that to update the hook values, so we + // need to call it. + currentComponent.componentWillUpdate = function (p, s, c) { + if (this._force) { + let tmp = prevScu; + // Clear to avoid other sCU hooks from being called + prevScu = undefined; + updateHookState(p, s, c); + prevScu = tmp; + } + + if (prevCWU) prevCWU.call(this, p, s, c); }; - } - } - - if (hookState._actions.length) { - const initialValue = hookState._value[0]; - hookState._actions.some(action => { - hookState._value[0] = hookState._reducer(hookState._value[0], action); - }); - - hookState._didUpdate = !ObjectIs(initialValue, hookState._value[0]); - hookState._value = [hookState._value[0], hookState._value[1]]; - hookState._didExecute = true; - hookState._actions = []; - } - return hookState._value; -} + // This SCU has the purpose of bailing out after repeated updates + // to stateful hooks. + // we store the next value in _nextValue[0] and keep doing that for all + // state setters, if we have next states and + // all next states within a component end up being equal to their original state + // we are safe to bail out for this specific component. + /** + * + * @type {import('./internal').Component["shouldComponentUpdate"]} + */ + // @ts-ignore - We don't use TS to downtranspile + // eslint-disable-next-line no-inner-declarations + function updateHookState(p, s, c) { + if (!hookState._component.__hooks) return true; + + /** @type {(x: import('./internal').HookState) => x is import('./internal').ReducerHookState} */ + const isStateHook = x => !!x._component; + const stateHooks = + hookState._component.__hooks._list.filter(isStateHook); + + const allHooksEmpty = stateHooks.every(x => !x._nextValue); + // When we have no updated hooks in the component we invoke the previous SCU or + // traverse the VDOM tree further. + if (allHooksEmpty) { + return prevScu ? prevScu.call(this, p, s, c) : true; + } + + // We check whether we have components with a nextValue set that + // have values that aren't equal to one another this pushes + // us to update further down the tree + let shouldUpdate = hookState._component.props !== p; + stateHooks.forEach(hookItem => { + if (hookItem._nextValue) { + const currentValue = hookItem._value[0]; + hookItem._value = hookItem._nextValue; + hookItem._nextValue = undefined; + if (!ObjectIs(currentValue, hookItem._value[0])) + shouldUpdate = true; + } + }); -options._afterRender = (newVNode, oldVNode) => { - if (newVNode._component && newVNode._component.__hooks) { - const hooks = newVNode._component.__hooks._list; - const stateHooksThatExecuted = hooks.filter( - /** @type {(x: import('./internal').HookState) => x is import('./internal').ReducerHookState} */ - // @ts-expect-error - x => x._component && x._didExecute - ); + return prevScu + ? prevScu.call(this, p, s, c) || shouldUpdate + : shouldUpdate; + } - if ( - stateHooksThatExecuted.length && - !stateHooksThatExecuted.some(x => x._didUpdate) && - oldVNode.props === newVNode.props - ) { - newVNode._component.__hooks._pendingEffects = []; - newVNode._flags |= SKIP_CHILDREN; + currentComponent.shouldComponentUpdate = updateHookState; } - - stateHooksThatExecuted.some(hook => { - hook._didExecute = hook._didUpdate = false; - }); } - if (oldAfterRender) oldAfterRender(newVNode, oldVNode); -}; + return hookState._nextValue || hookState._value; +} /** * @param {import('./internal').Effect} callback diff --git a/hooks/src/internal.d.ts b/hooks/src/internal.d.ts index b5b6d66c99..c51fc13a50 100644 --- a/hooks/src/internal.d.ts +++ b/hooks/src/internal.d.ts @@ -55,6 +55,8 @@ export type HookState = interface BaseHookState { _value?: unknown; + _nextValue?: unknown; + _pendingValue?: unknown; _args?: unknown; _pendingArgs?: unknown; _component?: unknown; @@ -73,6 +75,7 @@ export interface EffectHookState extends BaseHookState { export interface MemoHookState extends BaseHookState { _value?: T; + _pendingValue?: T; _args?: unknown[]; _pendingArgs?: unknown[]; _factory?: () => T; @@ -80,12 +83,10 @@ export interface MemoHookState extends BaseHookState { export interface ReducerHookState extends BaseHookState { + _nextValue?: [S, StateUpdater]; _value?: [S, StateUpdater]; - _actions?: any[]; _component?: Component; _reducer?: Reducer; - _didExecute?: boolean; - _didUpdate?: boolean; } export interface ContextHookState extends BaseHookState { diff --git a/hooks/test/browser/useState.test.js b/hooks/test/browser/useState.test.js index bf179ca38f..4d391c209d 100644 --- a/hooks/test/browser/useState.test.js +++ b/hooks/test/browser/useState.test.js @@ -1,14 +1,7 @@ import { setupRerender, act } from 'preact/test-utils'; import { createElement, render, createContext, Component } from 'preact'; -import { afterAll, beforeAll, expect, vi } from 'vitest'; -import { - useState, - useContext, - useEffect, - useLayoutEffect, - useReducer, - useRef -} from 'preact/hooks'; +import { vi } from 'vitest'; +import { useState, useContext, useEffect } from 'preact/hooks'; import { setupScratch, teardown } from '../../../test/_util/helpers'; /** @jsx createElement */ @@ -77,12 +70,12 @@ describe('useState', () => { doSetState(0); rerender(); expect(lastState).to.equal(0); - expect(Comp).toHaveBeenCalledTimes(2); + expect(Comp).toHaveBeenCalledOnce(); doSetState(() => 0); rerender(); expect(lastState).to.equal(0); - expect(Comp).toHaveBeenCalledTimes(3); + expect(Comp).toHaveBeenCalledOnce(); }); it('rerenders when setting the state', () => { @@ -352,7 +345,7 @@ describe('useState', () => { render(, scratch); }); - expect(renderSpy).toHaveBeenCalledTimes(3); + expect(renderSpy).toHaveBeenCalledTimes(2); }); it('Cancels effect invocations correctly when bailing', () => { @@ -388,7 +381,7 @@ describe('useState', () => { set('initial'); }); - expect(renderSpy).toHaveBeenCalledTimes(2); + expect(renderSpy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledOnce(); expect(cleanupSpy).not.toHaveBeenCalled(); }); @@ -418,7 +411,7 @@ describe('useState', () => { expect(scratch.innerHTML).to.equal('

hello world!!!

'); }); - it('should exhaust renders when NaN state is set as a result of a props update', () => { + it('should limit rerenders when setting state to NaN', () => { const calls = []; const App = ({ i }) => { calls.push('rendering' + i); @@ -440,55 +433,8 @@ describe('useState', () => { act(() => { render(, scratch); }); - expect(calls.length).to.equal(27); - expect(calls.slice(1).every(c => c === 'rendering2')).to.equal(true); - }); - - it('should bail correctly when setting NaN twice', () => { - const calls = []; - let set; - const Greeting = ({ greeting }) => { - calls.push('rendering ' + greeting); - return

{greeting}

; - }; - const App = () => { - const [greeting, setGreeting] = useState(0); - set = setGreeting; - - return ; - }; - - act(() => { - render(, scratch); - }); - expect(calls.length).to.equal(1); - expect(calls).to.deep.equal(['rendering 0']); - - act(() => { - set(1); - }); - expect(calls.length).to.equal(2); - expect(calls).to.deep.equal(['rendering 0', 'rendering 1']); - - act(() => { - set(NaN); - }); - expect(calls.length).to.equal(3); - expect(calls).to.deep.equal([ - 'rendering 0', - 'rendering 1', - 'rendering NaN' - ]); - - act(() => { - set(NaN); - }); expect(calls.length).to.equal(3); - expect(calls).to.deep.equal([ - 'rendering 0', - 'rendering 1', - 'rendering NaN' - ]); + expect(calls.slice(1).every(c => c === 'rendering2')).to.equal(true); }); describe('Global sCU', () => { @@ -532,94 +478,4 @@ describe('useState', () => { expect(renders).to.equal(2); }); }); - - it('Should capture the closure in the reducer', () => { - function createContext2() { - const context = createContext(); - - const ProviderOrig = context.Provider; - context.Provider = ({ value, children }) => { - const valueRef = useRef(value); - const contextValue = useRef(); - - if (!contextValue.current) { - contextValue.current = { - value: valueRef, - listener: null - }; - } - - useLayoutEffect(() => { - valueRef.current = value; - if (contextValue.current.listener) { - contextValue.current.listener([value]); - } - }, [value]); - return ( - {children} - ); - }; - - return context; - } - - function useContextSelector(context) { - const contextValue = useContext(context); - const { - value: { current: value } - } = contextValue; - const [state, dispatch] = useReducer( - () => { - return { - value - }; - }, - { - value - } - ); - useLayoutEffect(() => { - contextValue.listener = dispatch; - }, []); - return state.value; - } - - const context = createContext2(); - let set; - - function Child() { - const [count, setState] = useContextSelector(context); - const [c, setC] = useState(0); - set = () => { - setC(s => s + 1); - setState(s => s + 1); - }; - return ( -
-
Context count: {count}
-
Local count: {c}
-
- ); - } - - // Render this - function App() { - const [state, setState] = useState(0); - return ( - - - - ); - } - - act(() => { - render(, scratch); - }); - expect(scratch.textContent).to.equal('Context count: 0Local count: 0'); - - act(() => { - set(); - }); - expect(scratch.textContent).to.equal('Context count: 1Local count: 1'); - }); }); diff --git a/mangle.json b/mangle.json index bfe7a0b10b..d999d593f9 100644 --- a/mangle.json +++ b/mangle.json @@ -33,14 +33,12 @@ "$_list": "__", "$_pendingEffects": "__h", "$_value": "__", - "$_didExecute": "__N", - "$_didUpdate": "__U", + "$_nextValue": "__N", "$_original": "__v", "$_args": "__H", "$_factory": "__h", "$_depth": "__b", "$_dirty": "__d", - "$_afterRender": "__d", "$_mask": "__m", "$_detachOnNextRender": "__b", "$_force": "__e", diff --git a/src/constants.js b/src/constants.js index 57d56d98c6..2fe00830be 100644 --- a/src/constants.js +++ b/src/constants.js @@ -6,8 +6,6 @@ export const MODE_SUSPENDED = 1 << 7; export const INSERT_VNODE = 1 << 2; /** Indicates a VNode has been matched with another VNode in the diff */ export const MATCHED = 1 << 1; -/** Indicates that children should not be diffed */ -export const SKIP_CHILDREN = 1 << 3; /** Reset all mode flags */ export const RESET_MODE = ~(MODE_HYDRATE | MODE_SUSPENDED); diff --git a/src/diff/index.js b/src/diff/index.js index d9a62cbae5..30eed07d62 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -5,7 +5,6 @@ import { MODE_SUSPENDED, NULL, RESET_MODE, - SKIP_CHILDREN, SVG_NAMESPACE, UNDEFINED, XHTML_NAMESPACE @@ -224,7 +223,6 @@ export function diff( c._force = false; let renderHook = options._render, - afterRender = options._afterRender, count = 0; if (isClassComponent) { c.state = c._nextState; @@ -233,7 +231,6 @@ export function diff( if (renderHook) renderHook(newVNode); tmp = c.render(c.props, c.state, c.context); - if (afterRender) afterRender(newVNode, oldVNode); for (let i = 0; i < c._stateCallbacks.length; i++) { c._renderCallbacks.push(c._stateCallbacks[i]); @@ -245,18 +242,6 @@ export function diff( if (renderHook) renderHook(newVNode); tmp = c.render(c.props, c.state, c.context); - if (afterRender) afterRender(newVNode, oldVNode); - - if (newVNode._flags & SKIP_CHILDREN) { - c._dirty = false; - c._renderCallbacks = []; - newVNode._dom = oldVNode._dom; - newVNode._children = oldVNode._children; - newVNode._children.some(vnode => { - if (vnode) vnode._parent = newVNode; - }); - break outer; - } // Handle setState called in render, see #2553 c.state = c._nextState; diff --git a/src/internal.d.ts b/src/internal.d.ts index 403fe37dbb..72215d066a 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -27,8 +27,6 @@ export interface ErrorInfo { } export interface Options extends preact.Options { - /** Attach a hook that is invoked after a vnode has rendered. */ - _afterRender?(vnode: VNode, oldVNode: VNode): void; /** Attach a hook that is invoked before render, mainly to check the arguments. */ _root?(vnode: ComponentChild, parent: preact.ContainerNode): void; /** Attach a hook that is invoked before a vnode is diffed. */