diff --git a/compat/test/browser/context.test.js b/compat/test/browser/context.test.js new file mode 100644 index 0000000000..35046c88e5 --- /dev/null +++ b/compat/test/browser/context.test.js @@ -0,0 +1,144 @@ +import { setupRerender } from 'preact/test-utils'; +import { setupScratch, teardown } from '../../../test/_util/helpers'; +import React, { + render, + createElement, + createContext, + Component, + useState, + useContext +} from 'preact/compat'; + +describe('components', () => { + /** @type {HTMLDivElement} */ + let scratch; + + /** @type {() => void} */ + let rerender; + + beforeEach(() => { + scratch = setupScratch(); + rerender = setupRerender(); + }); + + afterEach(() => { + teardown(scratch); + }); + + it('nested context updates propagate throughout the tree synchronously', () => { + const RouterContext = createContext({ location: '__default_value__' }); + + const route1 = '/page/1'; + const route2 = '/page/2'; + + /** @type {() => void} */ + let toggleLocalState; + /** @type {() => void} */ + let toggleLocation; + + /** @type {Array<{location: string, localState: boolean}>} */ + let pageRenders = []; + + function runUpdate() { + toggleLocalState(); + toggleLocation(); + } + + /** + * @extends {React.Component<{children: any}, {location: string}>} + */ + class Router extends Component { + constructor(props) { + super(props); + this.state = { location: route1 }; + toggleLocation = () => { + const oldLocation = this.state.location; + const newLocation = oldLocation === route1 ? route2 : route1; + // console.log('Toggling location', oldLocation, '->', newLocation); + this.setState({ location: newLocation }); + }; + } + + render() { + // console.log('Rendering Router', { location: this.state.location }); + return ( + + {this.props.children} + + ); + } + } + + /** + * @extends {React.Component<{children: any}>} + */ + class Route extends Component { + render() { + return ( + + {contextValue => { + // console.log('Rendering Route', { + // location: contextValue.location + // }); + // Pretend to do something with the context value + const newContextValue = { ...contextValue }; + return ( + + {this.props.children} + + ); + }} + + ); + } + } + + function Page() { + const [localState, setLocalState] = useState(true); + const { location } = useContext(RouterContext); + + pageRenders.push({ location, localState }); + // console.log('Rendering Page', { location, localState }); + + toggleLocalState = () => { + let newValue = !localState; + // console.log('Toggling localState', localState, '->', newValue); + setLocalState(newValue); + }; + + return ( + <> +
localState: {localState.toString()}
+
location: {location}
+
+ +
+ + ); + } + + function App() { + return ( + + + + + + ); + } + + render(, scratch); + expect(pageRenders).to.deep.equal([{ location: route1, localState: true }]); + + pageRenders = []; + runUpdate(); // Simulate button click + rerender(); + + // Page should rerender once with both propagated context and local state updates + expect(pageRenders).to.deep.equal([ + { location: route2, localState: false } + ]); + }); +}); diff --git a/src/component.js b/src/component.js index f219acd7b5..7f39862206 100644 --- a/src/component.js +++ b/src/component.js @@ -202,16 +202,23 @@ export function enqueueRender(c) { /** Flush the render queue by rerendering all queued components */ function process() { - let queue; - while ((process._rerenderCount = rerenderQueue.length)) { - queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth); - rerenderQueue = []; - // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary - // process() calls from getting scheduled while `queue` is still being consumed. - queue.some(c => { - if (c._dirty) renderComponent(c); - }); + let c; + rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth); + // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary + // process() calls from getting scheduled while `queue` is still being consumed. + while ((c = rerenderQueue.shift())) { + if (c._dirty) { + let renderQueueLength = rerenderQueue.length; + renderComponent(c); + if (rerenderQueue.length > renderQueueLength) { + // When i.e. rerendering a provider additional new items can be injected, we want to + // keep the order from top to bottom with those new items so we can handle them in a + // single pass + rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth); + } + } } + process._rerenderCount = 0; } process._rerenderCount = 0;