mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
ReactDOM.useEvent: wire to event system to the hook (#18304)
This commit is contained in:
@@ -79,12 +79,20 @@ export function executeDispatchesInOrder(event) {
|
||||
validateEventDispatches(event);
|
||||
}
|
||||
if (Array.isArray(dispatchListeners)) {
|
||||
let previousInstance;
|
||||
for (let i = 0; i < dispatchListeners.length; i++) {
|
||||
if (event.isPropagationStopped()) {
|
||||
const instance = dispatchInstances[i];
|
||||
// We check if the instance was the same as the last one,
|
||||
// if it was, then we're still on the same instance thus
|
||||
// propagation should not stop. If we add support for
|
||||
// stopImmediatePropagation at some point, then we'll
|
||||
// need to handle that case here differently.
|
||||
if (instance !== previousInstance && event.isPropagationStopped()) {
|
||||
break;
|
||||
}
|
||||
// Listeners and Instances are two parallel arrays that are always in sync.
|
||||
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
|
||||
previousInstance = instance;
|
||||
}
|
||||
} else if (dispatchListeners) {
|
||||
executeDispatch(event, dispatchListeners, dispatchInstances);
|
||||
|
||||
@@ -470,6 +470,10 @@ export function beforeRemoveInstance(instance) {
|
||||
// noop
|
||||
}
|
||||
|
||||
export function registerEvent(event: any, rootContainerInstance: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
|
||||
import type {RootType} from './ReactDOMRoot';
|
||||
|
||||
import {
|
||||
precacheFiberNode,
|
||||
updateFiberProps,
|
||||
getClosestInstanceFromNode,
|
||||
getListenersFromTarget,
|
||||
} from './ReactDOMComponentTree';
|
||||
import {
|
||||
createElement,
|
||||
@@ -78,7 +80,9 @@ import {
|
||||
detachElementListener,
|
||||
isDOMDocument,
|
||||
isDOMElement,
|
||||
listenToTopLevelEvent,
|
||||
} from '../events/DOMModernPluginEventSystem';
|
||||
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
|
||||
|
||||
export type ReactListenerEvent = ReactDOMListenerEvent;
|
||||
export type ReactListenerMap = ReactDOMListenerMap;
|
||||
@@ -529,6 +533,22 @@ export function beforeRemoveInstance(
|
||||
) {
|
||||
dispatchBeforeDetachedBlur(((instance: any): HTMLElement));
|
||||
}
|
||||
if (enableUseEventAPI) {
|
||||
// It's unfortunate that we have to do this cleanup, but
|
||||
// it's necessary otherwise we will leak the host instances
|
||||
// from the useEvent hook instances Map. We call destroy
|
||||
// on each listener to ensure we properly remove the instance
|
||||
// from the instances Map. Note: we have this Map so that we
|
||||
// can properly unmount instances when the function component
|
||||
// that the hook is attached to gets unmounted.
|
||||
const listenersSet = getListenersFromTarget(instance);
|
||||
if (listenersSet !== null) {
|
||||
const listeners = Array.from(listenersSet);
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
listeners[i].destroy(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function removeChild(
|
||||
@@ -1083,6 +1103,23 @@ export function getInstanceFromNode(node: HTMLElement): null | Object {
|
||||
return getClosestInstanceFromNode(node) || null;
|
||||
}
|
||||
|
||||
export function registerEvent(
|
||||
event: ReactDOMListenerEvent,
|
||||
rootContainerInstance: Container,
|
||||
): void {
|
||||
const {passive, priority, type} = event;
|
||||
const listenerMap = getListenerMapForElement(rootContainerInstance);
|
||||
// Add the event listener to the target container (falling back to
|
||||
// the target if we didn't find one).
|
||||
listenToTopLevelEvent(
|
||||
((type: any): DOMTopLevelEventType),
|
||||
rootContainerInstance,
|
||||
listenerMap,
|
||||
passive,
|
||||
priority,
|
||||
);
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: ReactDOMListener): void {
|
||||
if (enableUseEventAPI) {
|
||||
const {target} = listener;
|
||||
|
||||
+1
-1
@@ -191,7 +191,7 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
|
||||
nativeEvent,
|
||||
nativeEventTarget,
|
||||
);
|
||||
accumulateTwoPhaseListeners(event);
|
||||
accumulateTwoPhaseListeners(event, true);
|
||||
return event;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1074,5 +1074,530 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
expect(listenerMaps.length).toEqual(2);
|
||||
expect(listenerMaps[0]).toEqual(listenerMaps[1]);
|
||||
});
|
||||
|
||||
it('can render correctly with the ReactDOMServer', () => {
|
||||
const clickEvent = jest.fn();
|
||||
|
||||
function Test() {
|
||||
const divRef = React.useRef(null);
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(divRef.current, clickEvent);
|
||||
});
|
||||
|
||||
return <div ref={divRef}>Hello world</div>;
|
||||
}
|
||||
const output = ReactDOMServer.renderToString(<Test />);
|
||||
expect(output).toBe(`<div data-reactroot="">Hello world</div>`);
|
||||
});
|
||||
|
||||
it('can render correctly with the ReactDOMServer hydration', () => {
|
||||
const clickEvent = jest.fn();
|
||||
const spanRef = React.createRef();
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(spanRef.current, clickEvent);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span ref={spanRef}>Hello world</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const output = ReactDOMServer.renderToString(<Test />);
|
||||
expect(output).toBe(
|
||||
`<div data-reactroot=""><span>Hello world</span></div>`,
|
||||
);
|
||||
container.innerHTML = output;
|
||||
ReactDOM.hydrate(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
dispatchClickEvent(spanRef.current);
|
||||
expect(clickEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should correctly work for a basic "click" listener', () => {
|
||||
let log = [];
|
||||
const clickEvent = jest.fn(event => {
|
||||
log.push({
|
||||
eventPhase: event.eventPhase,
|
||||
type: event.type,
|
||||
currentTarget: event.currentTarget,
|
||||
target: event.target,
|
||||
});
|
||||
});
|
||||
const divRef = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(buttonRef.current, clickEvent);
|
||||
});
|
||||
|
||||
return (
|
||||
<button ref={buttonRef}>
|
||||
<div ref={divRef}>Click me!</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
expect(container.innerHTML).toBe('<button><div>Click me!</div></button>');
|
||||
|
||||
// Clicking the button should trigger the event callback
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(log).toEqual([
|
||||
{
|
||||
eventPhase: 3,
|
||||
type: 'click',
|
||||
currentTarget: buttonRef.current,
|
||||
target: divRef.current,
|
||||
},
|
||||
]);
|
||||
expect(clickEvent).toBeCalledTimes(1);
|
||||
|
||||
// Unmounting the container and clicking should not work
|
||||
ReactDOM.render(null, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
dispatchClickEvent(divElement);
|
||||
expect(clickEvent).toBeCalledTimes(1);
|
||||
|
||||
// Re-rendering the container and clicking should work
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(clickEvent).toBeCalledTimes(2);
|
||||
|
||||
log = [];
|
||||
|
||||
// Clicking the button should also work
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(log).toEqual([
|
||||
{
|
||||
eventPhase: 3,
|
||||
type: 'click',
|
||||
currentTarget: buttonRef.current,
|
||||
target: buttonRef.current,
|
||||
},
|
||||
]);
|
||||
|
||||
function Test2({clickEvent2}) {
|
||||
const click = ReactDOM.unstable_useEvent('click', clickEvent2);
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(buttonRef.current, clickEvent2);
|
||||
});
|
||||
|
||||
return (
|
||||
<button ref={buttonRef}>
|
||||
<div ref={divRef}>Click me!</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
let clickEvent2 = jest.fn();
|
||||
ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(clickEvent2).toBeCalledTimes(1);
|
||||
|
||||
// Reset the function we pass in, so it's different
|
||||
clickEvent2 = jest.fn();
|
||||
ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(clickEvent2).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should correctly work for setting and clearing a basic "click" listener', () => {
|
||||
const clickEvent = jest.fn();
|
||||
const divRef = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
function Test({off}) {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(buttonRef.current, clickEvent);
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (off) {
|
||||
click.setListener(buttonRef.current, null);
|
||||
}
|
||||
}, [off]);
|
||||
|
||||
return (
|
||||
<button ref={buttonRef}>
|
||||
<div ref={divRef}>Click me!</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test off={false} />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(clickEvent).toBeCalledTimes(1);
|
||||
|
||||
// The listener should get unmounted in the second effect
|
||||
ReactDOM.render(<Test off={true} />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
clickEvent.mockClear();
|
||||
|
||||
divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(clickEvent).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('handle propagation of click events', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const log = [];
|
||||
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onClickCapture = jest.fn(e =>
|
||||
log.push(['capture', e.currentTarget]),
|
||||
);
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
const clickCapture = ReactDOM.unstable_useEvent('click', {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(buttonRef.current, onClick);
|
||||
clickCapture.setListener(buttonRef.current, onClickCapture);
|
||||
click.setListener(divRef.current, onClick);
|
||||
clickCapture.setListener(divRef.current, onClickCapture);
|
||||
});
|
||||
|
||||
return (
|
||||
<button ref={buttonRef}>
|
||||
<div ref={divRef}>Click me!</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[2]).toEqual(['capture', buttonElement]);
|
||||
expect(log[3]).toEqual(['capture', divElement]);
|
||||
expect(log[4]).toEqual(['bubble', divElement]);
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
});
|
||||
|
||||
it('should correctly work for a basic "click" listener on the outer target', () => {
|
||||
const log = [];
|
||||
const clickEvent = jest.fn(event => {
|
||||
log.push({
|
||||
eventPhase: event.eventPhase,
|
||||
type: event.type,
|
||||
currentTarget: event.currentTarget,
|
||||
target: event.target,
|
||||
});
|
||||
});
|
||||
const divRef = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(divRef.current, clickEvent);
|
||||
});
|
||||
|
||||
return (
|
||||
<button ref={buttonRef}>
|
||||
<div ref={divRef}>Click me!</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
expect(container.innerHTML).toBe('<button><div>Click me!</div></button>');
|
||||
|
||||
// Clicking the button should trigger the event callback
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(log).toEqual([
|
||||
{
|
||||
eventPhase: 3,
|
||||
type: 'click',
|
||||
currentTarget: divRef.current,
|
||||
target: divRef.current,
|
||||
},
|
||||
]);
|
||||
|
||||
// Unmounting the container and clicking should not work
|
||||
ReactDOM.render(null, container);
|
||||
dispatchClickEvent(divElement);
|
||||
expect(clickEvent).toBeCalledTimes(1);
|
||||
|
||||
// Re-rendering the container and clicking should work
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(clickEvent).toBeCalledTimes(2);
|
||||
|
||||
// Clicking the button should not work
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(clickEvent).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should correctly handle many nested target listeners', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const targetListener1 = jest.fn();
|
||||
const targetListener2 = jest.fn();
|
||||
const targetListener3 = jest.fn();
|
||||
const targetListener4 = jest.fn();
|
||||
|
||||
function Test() {
|
||||
const click1 = ReactDOM.unstable_useEvent('click', {capture: true});
|
||||
const click2 = ReactDOM.unstable_useEvent('click', {capture: true});
|
||||
const click3 = ReactDOM.unstable_useEvent('click');
|
||||
const click4 = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click1.setListener(buttonRef.current, targetListener1);
|
||||
click2.setListener(buttonRef.current, targetListener2);
|
||||
click3.setListener(buttonRef.current, targetListener3);
|
||||
click4.setListener(buttonRef.current, targetListener4);
|
||||
});
|
||||
|
||||
return <button ref={buttonRef}>Click me!</button>;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
|
||||
expect(targetListener1).toHaveBeenCalledTimes(1);
|
||||
expect(targetListener2).toHaveBeenCalledTimes(1);
|
||||
expect(targetListener3).toHaveBeenCalledTimes(1);
|
||||
expect(targetListener4).toHaveBeenCalledTimes(1);
|
||||
|
||||
function Test2() {
|
||||
const click1 = ReactDOM.unstable_useEvent('click');
|
||||
const click2 = ReactDOM.unstable_useEvent('click');
|
||||
const click3 = ReactDOM.unstable_useEvent('click');
|
||||
const click4 = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click1.setListener(buttonRef.current, targetListener1);
|
||||
click2.setListener(buttonRef.current, targetListener2);
|
||||
click3.setListener(buttonRef.current, targetListener3);
|
||||
click4.setListener(buttonRef.current, targetListener4);
|
||||
});
|
||||
|
||||
return <button ref={buttonRef}>Click me!</button>;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test2 />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(targetListener1).toHaveBeenCalledTimes(2);
|
||||
expect(targetListener2).toHaveBeenCalledTimes(2);
|
||||
expect(targetListener3).toHaveBeenCalledTimes(2);
|
||||
expect(targetListener4).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should correctly handle stopPropagation corrrectly for target events', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
let clickEvent = jest.fn();
|
||||
|
||||
function Test() {
|
||||
const click1 = ReactDOM.unstable_useEvent('click', {
|
||||
bind: buttonRef,
|
||||
});
|
||||
const click2 = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click1.setListener(buttonRef.current, clickEvent);
|
||||
click2.setListener(divRef.current, e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<button ref={buttonRef}>
|
||||
<div ref={divRef}>Click me!</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(clickEvent).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should correctly handle stopPropagation corrrectly for many target events', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const targetListerner1 = jest.fn(e => e.stopPropagation());
|
||||
const targetListerner2 = jest.fn(e => e.stopPropagation());
|
||||
const targetListerner3 = jest.fn(e => e.stopPropagation());
|
||||
const targetListerner4 = jest.fn(e => e.stopPropagation());
|
||||
|
||||
function Test() {
|
||||
const click1 = ReactDOM.unstable_useEvent('click');
|
||||
const click2 = ReactDOM.unstable_useEvent('click');
|
||||
const click3 = ReactDOM.unstable_useEvent('click');
|
||||
const click4 = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click1.setListener(buttonRef.current, targetListerner1);
|
||||
click2.setListener(buttonRef.current, targetListerner2);
|
||||
click3.setListener(buttonRef.current, targetListerner3);
|
||||
click4.setListener(buttonRef.current, targetListerner4);
|
||||
});
|
||||
|
||||
return <button ref={buttonRef}>Click me!</button>;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(targetListerner1).toHaveBeenCalledTimes(1);
|
||||
expect(targetListerner2).toHaveBeenCalledTimes(1);
|
||||
expect(targetListerner3).toHaveBeenCalledTimes(1);
|
||||
expect(targetListerner4).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should correctly handle stopPropagation for mixed capture/bubbling target listeners', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const targetListerner1 = jest.fn(e => e.stopPropagation());
|
||||
const targetListerner2 = jest.fn(e => e.stopPropagation());
|
||||
const targetListerner3 = jest.fn(e => e.stopPropagation());
|
||||
const targetListerner4 = jest.fn(e => e.stopPropagation());
|
||||
|
||||
function Test() {
|
||||
const click1 = ReactDOM.unstable_useEvent('click', {capture: true});
|
||||
const click2 = ReactDOM.unstable_useEvent('click', {capture: true});
|
||||
const click3 = ReactDOM.unstable_useEvent('click');
|
||||
const click4 = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click1.setListener(buttonRef.current, targetListerner1);
|
||||
click2.setListener(buttonRef.current, targetListerner2);
|
||||
click3.setListener(buttonRef.current, targetListerner3);
|
||||
click4.setListener(buttonRef.current, targetListerner4);
|
||||
});
|
||||
|
||||
return <button ref={buttonRef}>Click me!</button>;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(targetListerner1).toHaveBeenCalledTimes(1);
|
||||
expect(targetListerner2).toHaveBeenCalledTimes(1);
|
||||
expect(targetListerner3).toHaveBeenCalledTimes(1);
|
||||
expect(targetListerner4).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.experimental('should work with concurrent mode updates', async () => {
|
||||
const log = [];
|
||||
const ref = React.createRef();
|
||||
|
||||
function Test({counter}) {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
click.setListener(ref.current, () => {
|
||||
log.push({counter});
|
||||
});
|
||||
});
|
||||
|
||||
Scheduler.unstable_yieldValue('Test');
|
||||
return <button ref={ref}>Press me</button>;
|
||||
}
|
||||
|
||||
let root = ReactDOM.createRoot(container);
|
||||
root.render(<Test counter={0} />);
|
||||
|
||||
// Dev double-render
|
||||
if (__DEV__) {
|
||||
expect(Scheduler).toFlushAndYield(['Test', 'Test']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield(['Test']);
|
||||
}
|
||||
|
||||
// Click the button
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 0}]);
|
||||
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
|
||||
// Increase counter
|
||||
root.render(<Test counter={1} />);
|
||||
// Yield before committing
|
||||
// Dev double-render
|
||||
if (__DEV__) {
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Test', 'Test']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Test']);
|
||||
}
|
||||
|
||||
// Click the button again
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 0}]);
|
||||
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
|
||||
// Commit
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 1}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,11 +9,15 @@
|
||||
|
||||
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
|
||||
|
||||
import getListener from 'legacy-events/getListener';
|
||||
import {HostComponent} from 'shared/ReactWorkTags';
|
||||
import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import getListener from 'legacy-events/getListener';
|
||||
import {getListenersFromTarget} from '../client/ReactDOMComponentTree';
|
||||
|
||||
export default function accumulateTwoPhaseListeners(
|
||||
event: ReactSyntheticEvent,
|
||||
accumulateUseEventListeners?: boolean,
|
||||
): void {
|
||||
const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
|
||||
if (phasedRegistrationNames == null) {
|
||||
@@ -28,6 +32,33 @@ export default function accumulateTwoPhaseListeners(
|
||||
while (node !== null) {
|
||||
// We only care for listeners that are on HostComponents (i.e. <div>)
|
||||
if (node.tag === HostComponent) {
|
||||
// For useEvent listenrs
|
||||
if (enableUseEventAPI && accumulateUseEventListeners) {
|
||||
// useEvent event listeners
|
||||
const instance = node.stateNode;
|
||||
const targetType = event.type;
|
||||
const listeners = getListenersFromTarget(instance);
|
||||
|
||||
if (listeners !== null) {
|
||||
const listenersArr = Array.from(listeners);
|
||||
for (let i = 0; i < listenersArr.length; i++) {
|
||||
const listener = listenersArr[i];
|
||||
const {
|
||||
callback,
|
||||
event: {capture, type},
|
||||
} = listener;
|
||||
if (type === targetType) {
|
||||
if (capture === true) {
|
||||
dispatchListeners.unshift(callback);
|
||||
dispatchInstances.unshift(node);
|
||||
} else {
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Standard React on* listeners, i.e. onClick prop
|
||||
const captureListener = getListener(node, captured);
|
||||
if (captureListener != null) {
|
||||
|
||||
@@ -474,6 +474,10 @@ export function beforeRemoveInstance(instance: any) {
|
||||
// noop
|
||||
}
|
||||
|
||||
export function registerEvent(event: any, rootContainerInstance: Container) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
@@ -518,6 +518,10 @@ export function beforeRemoveInstance(instance: any) {
|
||||
// noop
|
||||
}
|
||||
|
||||
export function registerEvent(event: any, rootContainerInstance: Container) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
enableSuspenseCallback,
|
||||
enableScopeAPI,
|
||||
runAllPassiveEffectDestroysBeforeCreates,
|
||||
enableUseEventAPI,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -1053,6 +1054,8 @@ function commitUnmount(
|
||||
case HostComponent: {
|
||||
if (enableDeprecatedFlareAPI) {
|
||||
unmountDeprecatedResponderListeners(current);
|
||||
}
|
||||
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
|
||||
beforeRemoveInstance(current.stateNode);
|
||||
}
|
||||
safelyDetachRef(current);
|
||||
|
||||
+86
-3
@@ -24,6 +24,7 @@ import type {FiberRoot} from './ReactFiberRoot';
|
||||
import type {
|
||||
ReactListenerEvent,
|
||||
ReactListenerMap,
|
||||
ReactListener,
|
||||
} from './ReactFiberHostConfig';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
@@ -53,6 +54,12 @@ import {
|
||||
markRenderEventTimeAndConfig,
|
||||
markUnprocessedUpdateTime,
|
||||
} from './ReactFiberWorkLoop';
|
||||
import {
|
||||
registerEvent,
|
||||
mountEventListener as mountHostEventListener,
|
||||
unmountEventListener as unmountHostEventListener,
|
||||
validateEventListenerTarget,
|
||||
} from './ReactFiberHostConfig';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
import getComponentName from 'shared/getComponentName';
|
||||
@@ -73,6 +80,7 @@ import {
|
||||
setWorkInProgressVersion,
|
||||
warnAboutMultipleRenderersDEV,
|
||||
} from './ReactMutableSource';
|
||||
import {getRootHostContainer} from './ReactFiberHostContext';
|
||||
|
||||
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
|
||||
|
||||
@@ -1627,18 +1635,93 @@ function dispatchAction<S, A>(
|
||||
|
||||
const noOpMount = () => {};
|
||||
|
||||
function validateNotInFunctionRender(): boolean {
|
||||
if (currentlyRenderingFiber === null) {
|
||||
return true;
|
||||
}
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'Event listener methods from useEvent() cannot be called during render.' +
|
||||
' These methods should be called in an effect or event callback outside the render.',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createReactListener(
|
||||
event: ReactListenerEvent,
|
||||
callback: Event => void,
|
||||
target: EventTarget,
|
||||
destroy: Node => void,
|
||||
): ReactListener {
|
||||
return {
|
||||
callback,
|
||||
destroy,
|
||||
event,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
|
||||
if (enableUseEventAPI) {
|
||||
const hook = mountWorkInProgressHook();
|
||||
const listenerMap: Map<EventTarget, ReactListener> = new Map();
|
||||
const rootContainerInstance = getRootHostContainer();
|
||||
|
||||
// Register the event to the current root to ensure event
|
||||
// replaying can pick up the event ahead of time.
|
||||
registerEvent(event, rootContainerInstance);
|
||||
|
||||
const clear = () => {
|
||||
// TODO
|
||||
if (validateNotInFunctionRender()) {
|
||||
const listeners = Array.from(listenerMap.values());
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
unmountHostEventListener(listeners[i]);
|
||||
}
|
||||
listenerMap.clear();
|
||||
}
|
||||
};
|
||||
|
||||
const destroy = (target: Node) => {
|
||||
// We don't need to call detachListenerFromInstance
|
||||
// here as this method should only ever be called
|
||||
// from renderers that need to remove the instance
|
||||
// from the map representing an instance that still
|
||||
// holds a reference to the listenerMap. This means
|
||||
// things like "window" listeners on ReactDOM should
|
||||
// never enter this call path as the the instance in
|
||||
// those cases would be that of "window", which
|
||||
// should be handled via an optimized route in the
|
||||
// renderer, making less overhead here. If we change
|
||||
// this heuristic we should update this path to make
|
||||
// sure we call detachListenerFromInstance.
|
||||
listenerMap.delete(target);
|
||||
};
|
||||
|
||||
const reactListenerMap: ReactListenerMap = {
|
||||
clear,
|
||||
setListener(instance: EventTarget, callback: ?(Event) => void): void {
|
||||
// TODO
|
||||
setListener(target: EventTarget, callback: ?(Event) => void): void {
|
||||
if (
|
||||
validateNotInFunctionRender() &&
|
||||
validateEventListenerTarget(target, callback)
|
||||
) {
|
||||
let listener = listenerMap.get(target);
|
||||
if (listener === undefined) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
listener = createReactListener(event, callback, target, destroy);
|
||||
listenerMap.set(target, listener);
|
||||
} else {
|
||||
if (callback == null) {
|
||||
listenerMap.delete(target);
|
||||
unmountHostEventListener(listener);
|
||||
return;
|
||||
}
|
||||
listener.callback = callback;
|
||||
}
|
||||
mountHostEventListener(listener);
|
||||
}
|
||||
},
|
||||
};
|
||||
// In order to clear up upon the hook unmounting,
|
||||
|
||||
@@ -76,6 +76,7 @@ export const shouldUpdateFundamentalComponent =
|
||||
$$$hostConfig.shouldUpdateFundamentalComponent;
|
||||
export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode;
|
||||
export const beforeRemoveInstance = $$$hostConfig.beforeRemoveInstance;
|
||||
export const registerEvent = $$$hostConfig.registerEvent;
|
||||
export const mountEventListener = $$$hostConfig.mountEventListener;
|
||||
export const unmountEventListener = $$$hostConfig.unmountEventListener;
|
||||
export const validateEventListenerTarget =
|
||||
|
||||
@@ -380,6 +380,10 @@ export function beforeRemoveInstance(instance: any) {
|
||||
// noop
|
||||
}
|
||||
|
||||
export function registerEvent(event: any, rootContainerInstance: Container) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user