JSTQL-JS-Transform/output_testing/282useSyncExternalStoreShared-test.js

917 lines
33 KiB
JavaScript
Raw Normal View History

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let useSyncExternalStore;
let useSyncExternalStoreWithSelector;
let React;
let ReactDOM;
let ReactDOMClient;
let Scheduler;
let act;
let useState;
let useEffect;
let useLayoutEffect;
let assertLog;
let originalError;
// This tests shared behavior between the built-in and shim implementations of
// of useSyncExternalStore.
'Shared useSyncExternalStore behavior (shim and built-in)' |> describe(%, () => {
(() => {
jest.resetModules();
if ((flags => flags.enableUseSyncExternalStoreShim) |> gate(%)) {
// Test the shim against React 17.
'react' |> jest.mock(%, () => {
return (__DEV__ ? 'react-17/umd/react.development.js' : 'react-17/umd/react.production.min.js') |> jest.requireActual(%);
});
// Because React 17 prints extra logs we need to ignore them.
'react-dom' |> jest.mock(%, () => (__DEV__ ? 'react-dom-17/umd/react-dom.development.js' : 'react-dom-17/umd/react-dom.production.min.js') |> jest.requireActual(%));
originalError = console.error;
console.error = jest.fn();
}
React = 'react' |> require(%);
ReactDOM = 'react-dom' |> require(%);
ReactDOMClient = 'react-dom/client' |> require(%);
Scheduler = 'scheduler' |> require(%);
useState = React.useState;
useEffect = React.useEffect;
useLayoutEffect = React.useLayoutEffect;
const InternalTestUtils = 'internal-test-utils' |> require(%);
assertLog = InternalTestUtils.assertLog;
const internalAct = ('internal-test-utils' |> require(%)).act;
// The internal act implementation doesn't batch updates by default, since
// it's mostly used to test concurrent mode. But since these tests run
// in both concurrent and legacy mode, I'm adding batching here.
act = cb => (() => cb |> ReactDOM.unstable_batchedUpdates(%)) |> internalAct(%);
if ((flags => flags.source) |> gate(%)) {
// The `shim/with-selector` module composes the main
// `use-sync-external-store` entrypoint. In the compiled artifacts, this
// is resolved to the `shim` implementation by our build config, but when
// running the tests against the source files, we need to tell Jest how to
// resolve it. Because this is a source module, this mock has no affect on
// the build tests.
'use-sync-external-store/src/useSyncExternalStore' |> jest.mock(%, () => 'use-sync-external-store/shim' |> jest.requireActual(%));
}
useSyncExternalStore = ('use-sync-external-store/shim' |> require(%)).useSyncExternalStore;
useSyncExternalStoreWithSelector = ('use-sync-external-store/shim/with-selector' |> require(%)).useSyncExternalStoreWithSelector;
}) |> beforeEach(%);
(() => {
if ((flags => flags.enableUseSyncExternalStoreShim) |> gate(%)) {
console.error = originalError;
}
}) |> afterEach(%);
function Text({
text
}) {
text |> Scheduler.log(%);
return text;
}
function createRoot(container) {
// This wrapper function exists so we can test both legacy roots and
// concurrent roots.
if ((flags => !flags.enableUseSyncExternalStoreShim) |> gate(%)) {
// The native implementation only exists in 18+, so we test using
// concurrent mode. To test the legacy root behavior in the native
// implementation (which is supported in the sense that it needs to have
// the correct behavior, despite the fact that the legacy root API
// triggers a warning in 18), write a test that uses
// createLegacyRoot directly.
return container |> ReactDOMClient.createRoot(%);
} else {
// This ReactDOM.render is from the React 17 npm module.
null |> ReactDOM.render(%, container);
return {
render(children) {
children |> ReactDOM.render(%, container);
}
};
}
}
function createExternalStore(initialState) {
const listeners = new Set();
let currentState = initialState;
return {
set(text) {
currentState = text;
(() => {
(listener => listener()) |> listeners.forEach(%);
}) |> ReactDOM.unstable_batchedUpdates(%);
},
subscribe(listener) {
listener |> listeners.add(%);
return () => listener |> listeners.delete(%);
},
getState() {
return currentState;
},
getSubscriberCount() {
return listeners.size;
}
};
}
'basic usage' |> it(%, async () => {
const store = 'Initial' |> createExternalStore(%);
function App() {
const text = store.subscribe |> useSyncExternalStore(%, store.getState);
return Text |> React.createElement(%, {
text: text
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
['Initial'] |> assertLog(%);
'Initial' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
'Updated' |> store.set(%);
}) |> act(%));
['Updated'] |> assertLog(%);
'Updated' |> (container.textContent |> expect(%)).toEqual(%);
});
'skips re-rendering if nothing changes' |> it(%, async () => {
const store = 'Initial' |> createExternalStore(%);
function App() {
const text = store.subscribe |> useSyncExternalStore(%, store.getState);
return Text |> React.createElement(%, {
text: text
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
['Initial'] |> assertLog(%);
// Update to the same value
'Initial' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
'Initial' |> store.set(%);
}) |> act(%));
// Should not re-render
[] |> assertLog(%);
'Initial' |> (container.textContent |> expect(%)).toEqual(%);
});
'switch to a different store' |> it(%, async () => {
const storeA = 0 |> createExternalStore(%);
const storeB = 0 |> createExternalStore(%);
let setStore;
function App() {
const [store, _setStore] = storeA |> useState(%);
setStore = _setStore;
const value = store.subscribe |> useSyncExternalStore(%, store.getState);
return Text |> React.createElement(%, {
text: value
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
[0] |> assertLog(%);
'0' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
1 |> storeA.set(%);
}) |> act(%));
[1] |> assertLog(%);
// Switch stores and update in the same batch
'1' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
(() => {
// This update will be disregarded
2 |> storeA.set(%);
storeB |> setStore(%);
}) |> ReactDOM.flushSync(%);
}) |> act(%));
// Now reading from B instead of A
[0] |> assertLog(%);
// Update A
'0' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
3 |> storeA.set(%);
}) |> act(%));
// Nothing happened, because we're no longer subscribed to A
[] |> assertLog(%);
// Update B
'0' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
1 |> storeB.set(%);
}) |> act(%));
[1] |> assertLog(%);
'1' |> (container.textContent |> expect(%)).toEqual(%);
});
// In React 18, you can't observe in between a sync render and its
// passive effects, so this is only relevant to legacy roots
// @gate enableUseSyncExternalStoreShim
'selecting a specific value inside getSnapshot' |> it(%, async () => {
const store = {
a: 0,
b: 0
} |> createExternalStore(%);
function A() {
const a = store.subscribe |> useSyncExternalStore(%, () => store.getState().a);
return Text |> React.createElement(%, {
text: 'A' + a
});
}
function B() {
const b = store.subscribe |> useSyncExternalStore(%, () => store.getState().b);
return Text |> React.createElement(%, {
text: 'B' + b
});
}
function App() {
return React.createElement(React.Fragment, null, A |> React.createElement(%, null), B |> React.createElement(%, null));
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
['A0', 'B0'] |> assertLog(%);
// Update b but not a
'A0B0' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
({
a: 0,
b: 1
}) |> store.set(%);
}) |> act(%));
// Only b re-renders
['B1'] |> assertLog(%);
// Update a but not b
'A0B1' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
({
a: 1,
b: 1
}) |> store.set(%);
}) |> act(%));
// Only a re-renders
['A1'] |> assertLog(%);
'A1B1' |> (container.textContent |> expect(%)).toEqual(%);
});
"compares to current state before bailing out, even when there's a " + 'mutation in between the sync and passive effects' |> it(%, async () => {
const store = 0 |> createExternalStore(%);
function App() {
const value = store.subscribe |> useSyncExternalStore(%, store.getState);
(() => {
'Passive effect: ' + value |> Scheduler.log(%);
}) |> useEffect(%, [value]);
return Text |> React.createElement(%, {
text: value
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
// Schedule an update. We'll intentionally not use `act` so that we can
// insert a mutation before React subscribes to the store in a
// passive effect.
[0, 'Passive effect: 0'] |> assertLog(%);
1 |> store.set(%);
[1
// Passive effect hasn't fired yet
] |> assertLog(%);
// Flip the store state back to the previous value.
'1' |> (container.textContent |> expect(%)).toEqual(%);
0 |> store.set(%);
// Should flip back to 0
['Passive effect: 1',
// Re-render. If the current state were tracked by updating a ref in a
// passive effect, then this would break because the previous render's
// passive effect hasn't fired yet, so we'd incorrectly think that
// the state hasn't changed.
0] |> assertLog(%);
'0' |> (container.textContent |> expect(%)).toEqual(%);
});
'mutating the store in between render and commit when getSnapshot has changed' |> it(%, async () => {
const store = {
a: 1,
b: 1
} |> createExternalStore(%);
const getSnapshotA = () => store.getState().a;
const getSnapshotB = () => store.getState().b;
function Child1({
step
}) {
const value = store.subscribe |> useSyncExternalStore(%, store.getState);
(() => {
if (step === 1) {
// Update B in a layout effect. This happens in the same commit
// that changed the getSnapshot in Child2. Child2's effects haven't
// fired yet, so it doesn't have access to the latest getSnapshot. So
// it can't use the getSnapshot to bail out.
'Update B in commit phase' |> Scheduler.log(%);
({
a: value.a,
b: 2
}) |> store.set(%);
}
}) |> useLayoutEffect(%, [step]);
return null;
}
function Child2({
step
}) {
const label = step === 0 ? 'A' : 'B';
const getSnapshot = step === 0 ? getSnapshotA : getSnapshotB;
const value = store.subscribe |> useSyncExternalStore(%, getSnapshot);
return Text |> React.createElement(%, {
text: label + value
});
}
let setStep;
function App() {
const [step, _setStep] = 0 |> useState(%);
setStep = _setStep;
return React.createElement(React.Fragment, null, Child1 |> React.createElement(%, {
step: step
}), Child2 |> React.createElement(%, {
step: step
}));
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
['A1'] |> assertLog(%);
'A1' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
// Change getSnapshot and update the store in the same batch
1 |> setStep(%);
}) |> act(%));
['B1', 'Update B in commit phase',
// If Child2 had used the old getSnapshot to bail out, then it would have
// incorrectly bailed out here instead of re-rendering.
'B2'] |> assertLog(%);
'B2' |> (container.textContent |> expect(%)).toEqual(%);
});
'mutating the store in between render and commit when getSnapshot has _not_ changed' |> it(%, async () => {
// Same as previous test, but `getSnapshot` does not change
const store = {
a: 1,
b: 1
} |> createExternalStore(%);
const getSnapshotA = () => store.getState().a;
function Child1({
step
}) {
const value = store.subscribe |> useSyncExternalStore(%, store.getState);
(() => {
if (step === 1) {
// Update B in a layout effect. This happens in the same commit
// that changed the getSnapshot in Child2. Child2's effects haven't
// fired yet, so it doesn't have access to the latest getSnapshot. So
// it can't use the getSnapshot to bail out.
'Update B in commit phase' |> Scheduler.log(%);
({
a: value.a,
b: 2
}) |> store.set(%);
}
}) |> useLayoutEffect(%, [step]);
return null;
}
function Child2({
step
}) {
const value = store.subscribe |> useSyncExternalStore(%, getSnapshotA);
return Text |> React.createElement(%, {
text: 'A' + value
});
}
let setStep;
function App() {
const [step, _setStep] = 0 |> useState(%);
setStep = _setStep;
return React.createElement(React.Fragment, null, Child1 |> React.createElement(%, {
step: step
}), Child2 |> React.createElement(%, {
step: step
}));
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
['A1'] |> assertLog(%);
// This will cause a layout effect, and in the layout effect we'll update
// the store
'A1' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
1 |> setStep(%);
}) |> act(%));
['A1',
// This updates B, but since Child2 doesn't subscribe to B, it doesn't
// need to re-render.
'Update B in commit phase'
// No re-render
] |> assertLog(%);
'A1' |> (container.textContent |> expect(%)).toEqual(%);
});
"does not bail out if the previous update hasn't finished yet" |> it(%, async () => {
const store = 0 |> createExternalStore(%);
function Child1() {
const value = store.subscribe |> useSyncExternalStore(%, store.getState);
(() => {
if (value === 1) {
'Reset back to 0' |> Scheduler.log(%);
0 |> store.set(%);
}
}) |> useLayoutEffect(%, [value]);
return Text |> React.createElement(%, {
text: value
});
}
function Child2() {
const value = store.subscribe |> useSyncExternalStore(%, store.getState);
return Text |> React.createElement(%, {
text: value
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => React.createElement(React.Fragment, null, Child1 |> React.createElement(%, null), Child2 |> React.createElement(%, null)) |> root.render(%)) |> act(%));
[0, 0] |> assertLog(%);
'00' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
1 |> store.set(%);
}) |> act(%));
[1, 1, 'Reset back to 0', 0, 0] |> assertLog(%);
'00' |> (container.textContent |> expect(%)).toEqual(%);
});
'uses the latest getSnapshot, even if it changed in the same batch as a store update' |> it(%, async () => {
const store = {
a: 0,
b: 0
} |> createExternalStore(%);
const getSnapshotA = () => store.getState().a;
const getSnapshotB = () => store.getState().b;
let setGetSnapshot;
function App() {
const [getSnapshot, _setGetSnapshot] = (() => getSnapshotA) |> useState(%);
setGetSnapshot = _setGetSnapshot;
const text = store.subscribe |> useSyncExternalStore(%, getSnapshot);
return Text |> React.createElement(%, {
text: text
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
// Update the store and getSnapshot at the same time
[0] |> assertLog(%);
await ((() => {
(() => {
(() => getSnapshotB) |> setGetSnapshot(%);
({
a: 1,
b: 2
}) |> store.set(%);
}) |> ReactDOM.flushSync(%);
}) |> act(%));
// It should read from B instead of A
[2] |> assertLog(%);
'2' |> (container.textContent |> expect(%)).toEqual(%);
});
'handles errors thrown by getSnapshot' |> it(%, async () => {
class ErrorBoundary extends React.Component {
state = {
error: null
};
static getDerivedStateFromError(error) {
return {
error
};
}
render() {
if (this.state.error) {
return Text |> React.createElement(%, {
text: this.state.error.message
});
}
return this.props.children;
}
}
const store = {
value: 0,
throwInGetSnapshot: false,
throwInIsEqual: false
} |> createExternalStore(%);
function App() {
const {
value
} = store.subscribe |> useSyncExternalStore(%, () => {
const state = store.getState();
if (state.throwInGetSnapshot) {
throw new Error('Error in getSnapshot');
}
return state;
});
return Text |> React.createElement(%, {
text: value
});
}
const errorBoundary = null |> React.createRef(%);
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => React.createElement(ErrorBoundary, {
ref: errorBoundary
}, App |> React.createElement(%, null)) |> root.render(%)) |> act(%));
[0] |> assertLog(%);
// Update that throws in a getSnapshot. We can catch it with an error boundary.
'0' |> (container.textContent |> expect(%)).toEqual(%);
if (__DEV__ && ((flags => flags.enableUseSyncExternalStoreShim) |> gate(%))) {
// In 17, the error is re-thrown in DEV.
await ('Error in getSnapshot' |> ((async () => {
await ((() => {
({
value: 1,
throwInGetSnapshot: true,
throwInIsEqual: false
}) |> store.set(%);
}) |> act(%));
}) |> expect(%)).rejects.toThrow(%));
} else {
await ((() => {
({
value: 1,
throwInGetSnapshot: true,
throwInIsEqual: false
}) |> store.set(%);
}) |> act(%));
}
((flags => flags.enableUseSyncExternalStoreShim) |> gate(%) ? ['Error in getSnapshot'] : ['Error in getSnapshot',
// In a concurrent root, React renders a second time to attempt to
// recover from the error.
'Error in getSnapshot']) |> assertLog(%);
'Error in getSnapshot' |> (container.textContent |> expect(%)).toEqual(%);
});
'Infinite loop if getSnapshot keeps returning new reference' |> it(%, async () => {
const store = {} |> createExternalStore(%);
function App() {
const text = store.subscribe |> useSyncExternalStore(%, () => ({}));
return Text |> React.createElement(%, {
text: text |> JSON.stringify(%)
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await (((flags => flags.enableUseSyncExternalStoreShim) |> gate(%) ? ['Maximum update depth exceeded. ', 'The result of getSnapshot should be cached to avoid an infinite loop', 'The above error occurred in the'] : ['The result of getSnapshot should be cached to avoid an infinite loop']) |> ((async () => {
await ('Maximum update depth exceeded. This can happen when a component repeatedly ' + 'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' + 'the number of nested updates to prevent infinite loops.' |> ((async () => {
await ((() => {
(async () => App |> React.createElement(%, null) |> root.render(%)) |> ReactDOM.flushSync(%);
}) |> act(%));
}) |> expect(%)).rejects.toThrow(%));
}) |> expect(%)).toErrorDev(%, {
withoutStack: (flags => {
if (flags.enableUseSyncExternalStoreShim) {
// Stacks don't work when mixing the source and the npm package.
return flags.source ? 1 : 0;
}
return false;
}) |> gate(%)
}));
});
'getSnapshot can return NaN without infinite loop warning' |> it(%, async () => {
const store = 'not a number' |> createExternalStore(%);
function App() {
const value = store.subscribe |> useSyncExternalStore(%, () => store.getState() |> parseInt(%, 10));
return Text |> React.createElement(%, {
text: value
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
// Initial render that reads a snapshot of NaN. This is OK because we use
// Object.is algorithm to compare values.
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
'NaN' |> (container.textContent |> expect(%)).toEqual(%);
// Update to real number
[NaN] |> assertLog(%);
await ((() => 123 |> store.set(%)) |> act(%));
'123' |> (container.textContent |> expect(%)).toEqual(%);
// Update back to NaN
[123] |> assertLog(%);
await ((() => 'not a number' |> store.set(%)) |> act(%));
'NaN' |> (container.textContent |> expect(%)).toEqual(%);
[NaN] |> assertLog(%);
});
'extra features implemented in user-space' |> describe(%, () => {
'memoized selectors are only called once per update' |> it(%, async () => {
const store = {
a: 0,
b: 0
} |> createExternalStore(%);
function selector(state) {
'Selector' |> Scheduler.log(%);
return state.a;
}
function App() {
'App' |> Scheduler.log(%);
const a = useSyncExternalStoreWithSelector(store.subscribe, store.getState, null, selector);
return Text |> React.createElement(%, {
text: 'A' + a
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
['App', 'Selector', 'A0'] |> assertLog(%);
// Update the store
'A0' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
({
a: 1,
b: 0
}) |> store.set(%);
}) |> act(%));
[
// The selector runs before React starts rendering
'Selector', 'App',
// And because the selector didn't change during render, we can reuse
// the previous result without running the selector again
'A1'] |> assertLog(%);
'A1' |> (container.textContent |> expect(%)).toEqual(%);
});
'Using isEqual to bailout' |> it(%, async () => {
const store = {
a: 0,
b: 0
} |> createExternalStore(%);
function A() {
const {
a
} = useSyncExternalStoreWithSelector(store.subscribe, store.getState, null, state => ({
a: state.a
}), (state1, state2) => state1.a === state2.a);
return Text |> React.createElement(%, {
text: 'A' + a
});
}
function B() {
const {
b
} = useSyncExternalStoreWithSelector(store.subscribe, store.getState, null, state => {
return {
b: state.b
};
}, (state1, state2) => state1.b === state2.b);
return Text |> React.createElement(%, {
text: 'B' + b
});
}
function App() {
return React.createElement(React.Fragment, null, A |> React.createElement(%, null), B |> React.createElement(%, null));
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
['A0', 'B0'] |> assertLog(%);
// Update b but not a
'A0B0' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
({
a: 0,
b: 1
}) |> store.set(%);
}) |> act(%));
// Only b re-renders
['B1'] |> assertLog(%);
// Update a but not b
'A0B1' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
({
a: 1,
b: 1
}) |> store.set(%);
}) |> act(%));
// Only a re-renders
['A1'] |> assertLog(%);
'A1B1' |> (container.textContent |> expect(%)).toEqual(%);
});
'basic server hydration' |> it(%, async () => {
const store = 'client' |> createExternalStore(%);
const ref = React.createRef();
function App() {
const text = useSyncExternalStore(store.subscribe, store.getState, () => 'server');
(() => {
'Passive effect: ' + text |> Scheduler.log(%);
}) |> useEffect(%, [text]);
return React.createElement('div', {
ref: ref
}, Text |> React.createElement(%, {
text: text
}));
}
const container = 'div' |> document.createElement(%);
container.innerHTML = '<div>server</div>';
const serverRenderedDiv = ('div' |> container.getElementsByTagName(%))[0];
if ((flags => !flags.enableUseSyncExternalStoreShim) |> gate(%)) {
await ((() => {
container |> ReactDOMClient.hydrateRoot(%, App |> React.createElement(%, null));
}) |> act(%));
[
// First it hydrates the server rendered HTML
'server', 'Passive effect: server',
// Then in a second paint, it re-renders with the client state
'client', 'Passive effect: client'] |> assertLog(%);
} else {
// In the userspace shim, there's no mechanism to detect whether we're
// currently hydrating, so `getServerSnapshot` is not called on the
// client. To avoid this server mismatch warning, user must account for
// this themselves and return the correct value inside `getSnapshot`.
await ((() => {
'Text content did not match' |> ((() => App |> React.createElement(%, null) |> ReactDOM.hydrate(%, container)) |> expect(%)).toErrorDev(%);
}) |> act(%));
['client', 'Passive effect: client'] |> assertLog(%);
}
'client' |> (container.textContent |> expect(%)).toEqual(%);
serverRenderedDiv |> (ref.current |> expect(%)).toEqual(%);
});
});
'regression test for #23150' |> it(%, async () => {
const store = 'Initial' |> createExternalStore(%);
function App() {
const text = store.subscribe |> useSyncExternalStore(%, store.getState);
const [derivedText, setDerivedText] = text |> useState(%);
(() => {}) |> useEffect(%, []);
if (derivedText !== text.toUpperCase()) {
text.toUpperCase() |> setDerivedText(%);
}
return Text |> React.createElement(%, {
text: derivedText
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => App |> React.createElement(%, null) |> root.render(%)) |> act(%));
['INITIAL'] |> assertLog(%);
'INITIAL' |> (container.textContent |> expect(%)).toEqual(%);
await ((() => {
'Updated' |> store.set(%);
}) |> act(%));
['UPDATED'] |> assertLog(%);
'UPDATED' |> (container.textContent |> expect(%)).toEqual(%);
});
'compares selection to rendered selection even if selector changes' |> it(%, async () => {
const store = {
items: ['A', 'B']
} |> createExternalStore(%);
const shallowEqualArray = (a, b) => {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
};
const List = (({
items
}) => {
return React.createElement('ul', null, (text => React.createElement('li', {
key: text
}, Text |> React.createElement(%, {
key: text,
text: text
}))) |> items.map(%));
}) |> React.memo(%);
function App({
step
}) {
const inlineSelector = state => {
'Inline selector' |> Scheduler.log(%);
return [...state.items, 'C'];
};
const items = useSyncExternalStoreWithSelector(store.subscribe, store.getState, null, inlineSelector, shallowEqualArray);
return React.createElement(React.Fragment, null, List |> React.createElement(%, {
items: items
}), Text |> React.createElement(%, {
text: 'Sibling: ' + step
}));
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => {
App |> React.createElement(%, {
step: 0
}) |> root.render(%);
}) |> act(%));
['Inline selector', 'A', 'B', 'C', 'Sibling: 0'] |> assertLog(%);
await ((() => {
App |> React.createElement(%, {
step: 1
}) |> root.render(%);
}) |> act(%));
[
// We had to call the selector again because it's not memoized
'Inline selector',
// But because the result was the same (according to isEqual) we can
// bail out of rendering the memoized list. These are skipped:
// 'A',
// 'B',
// 'C',
'Sibling: 1'] |> assertLog(%);
});
'selector and isEqual error handling in extra' |> describe(%, () => {
let ErrorBoundary;
(() => {
ErrorBoundary = class extends React.Component {
state = {
error: null
};
static getDerivedStateFromError(error) {
return {
error
};
}
render() {
if (this.state.error) {
return Text |> React.createElement(%, {
text: this.state.error.message
});
}
return this.props.children;
}
};
}) |> beforeEach(%);
'selector can throw on update' |> it(%, async () => {
const store = {
a: 'a'
} |> createExternalStore(%);
const selector = state => {
if (typeof state.a !== 'string') {
throw new TypeError('Malformed state');
}
return state.a.toUpperCase();
};
function App() {
const a = useSyncExternalStoreWithSelector(store.subscribe, store.getState, null, selector);
return Text |> React.createElement(%, {
text: a
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => React.createElement(ErrorBoundary, null, App |> React.createElement(%, null)) |> root.render(%)) |> act(%));
['A'] |> assertLog(%);
'A' |> (container.textContent |> expect(%)).toEqual(%);
if (__DEV__ && ((flags => flags.enableUseSyncExternalStoreShim) |> gate(%))) {
// In 17, the error is re-thrown in DEV.
await ('Malformed state' |> ((async () => {
await ((() => {
({}) |> store.set(%);
}) |> act(%));
}) |> expect(%)).rejects.toThrow(%));
} else {
await ((() => {
({}) |> store.set(%);
}) |> act(%));
}
'Malformed state' |> (container.textContent |> expect(%)).toEqual(%);
});
'isEqual can throw on update' |> it(%, async () => {
const store = {
a: 'A'
} |> createExternalStore(%);
const selector = state => state.a;
const isEqual = (left, right) => {
if (typeof left.a !== 'string' || typeof right.a !== 'string') {
throw new TypeError('Malformed state');
}
return left.a.trim() === right.a.trim();
};
function App() {
const a = useSyncExternalStoreWithSelector(store.subscribe, store.getState, null, selector, isEqual);
return Text |> React.createElement(%, {
text: a
});
}
const container = 'div' |> document.createElement(%);
const root = container |> createRoot(%);
await ((() => React.createElement(ErrorBoundary, null, App |> React.createElement(%, null)) |> root.render(%)) |> act(%));
['A'] |> assertLog(%);
'A' |> (container.textContent |> expect(%)).toEqual(%);
if (__DEV__ && ((flags => flags.enableUseSyncExternalStoreShim) |> gate(%))) {
// In 17, the error is re-thrown in DEV.
await ('Malformed state' |> ((async () => {
await ((() => {
({}) |> store.set(%);
}) |> act(%));
}) |> expect(%)).rejects.toThrow(%));
} else {
await ((() => {
({}) |> store.set(%);
}) |> act(%));
}
'Malformed state' |> (container.textContent |> expect(%)).toEqual(%);
});
});
});