Skip to content

Commit

Permalink
backport #3871
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Jan 12, 2024
1 parent da7c661 commit c73e072
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 7 deletions.
144 changes: 144 additions & 0 deletions compat/test/browser/context.test.js
Original file line number Diff line number Diff line change
@@ -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 (
<RouterContext.Provider value={{ location: this.state.location }}>
{this.props.children}
</RouterContext.Provider>
);
}
}

/**
* @extends {React.Component<{children: any}>}
*/
class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{(contextValue) => {
// console.log('Rendering Route', {
// location: contextValue.location
// });
// Pretend to do something with the context value
const newContextValue = { ...contextValue };
return (
<RouterContext.Provider value={newContextValue}>
{this.props.children}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}

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 (
<>
<div>localState: {localState.toString()}</div>
<div>location: {location}</div>
<div>
<button type="button" onClick={runUpdate}>
Trigger update
</button>
</div>
</>
);
}

function App() {
return (
<Router>
<Route>
<Page />
</Route>
</Router>
);
}

render(<App />, 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 }
]);
});
});
25 changes: 18 additions & 7 deletions src/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function Component(props, context) {
* @param {() => void} [callback] A function to be called once component state is
* updated
*/
Component.prototype.setState = function(update, callback) {
Component.prototype.setState = function (update, callback) {
// only clone state when copying to nextState the first time.
let s;
if (this._nextState != null && this._nextState !== this.state) {
Expand Down Expand Up @@ -64,7 +64,7 @@ Component.prototype.setState = function(update, callback) {
* @param {() => void} [callback] A function to be called after component is
* re-rendered
*/
Component.prototype.forceUpdate = function(callback) {
Component.prototype.forceUpdate = function (callback) {
const internal = this._internal;
if (internal) {
// Set render mode so that we can differentiate where the render request
Expand Down Expand Up @@ -141,11 +141,22 @@ export function enqueueRender(internal) {

/** Flush the render queue by rerendering all queued components */
function processRenderQueue() {
while ((len = processRenderQueue._rerenderCount = renderQueue.length)) {
renderQueue.sort((a, b) => a._depth - b._depth);
while (len--) {
renderQueuedInternal(renderQueue.shift());
let c;
renderQueue.sort((a, b) => a._depth - b._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 = renderQueue.shift())) {
if (c.flags & DIRTY_BIT) {
let renderQueueLength = renderQueue.length;
renderQueuedInternal(c);
if (renderQueue.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
renderQueue.sort((a, b) => a._depth - b._depth);
}
}
}
processRenderQueue._rerenderCount = 0;
}
let len = (processRenderQueue._rerenderCount = 0);
processRenderQueue._rerenderCount = 0;

0 comments on commit c73e072

Please sign in to comment.