I have researched synthetic events in React and I understand that React pools events to improve performance. I am also aware that events in React are not DOM events. I have read several posts and threads about this topic but I cannot find any mention of calling preventDefault after calling event.persist.
Many sites have mentioned that if we want to capture the value of event.target, for example, that one option is simply to cache it for later use but this does not apply to my use case.
I want to throttle an event handler that is listening for onDragOver events. In order to do this in React, I have to pass the event through 3 functions, calling event.persist on the first one so that the last one can see it.
However, event.preventDefault has no effect when I do call it. It's almost as if once we call event.persist, that's it and there's no turning back.
Below is some of the code but you may find it more helpful to experiment with it on StackBlitz.
import React, { Component } from 'react';
import { throttle } from 'throttle-debounce';
import DropItem from './DropItem';
class DropZone extends Component {
constructor(props) {
super(props);
this.onDragOverThrottled = throttle(500, this.onDragOver);
this.onDragStart = this.onDragStart.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
}
onDragStart(e, id) {
console.log('dragstart: ', id);
e.dataTransfer.setData("id", id);
}
onDragOver(e) {
e.preventDefault(); // this does nothing if event.persist fires before it
console.log('dragover...');
}
handleDragOver(e) {
e.persist();
this.onDragOverThrottled(e);
}
render() {
const items = this.props.items.map((item, index) => {
return <DropItem item={item} key={index} onDragStart={this.onDragStart} />;
});
return (
<div
className={this.props.class}
//onDragOver={this.handleDragOver} // See note 1 below
onDragOver={this.onDragOver} // See note 2 below
onDrop={(e) => {this.props.onDrop(e, this.props.location)}}>
<span className="task-header">{this.props.title}</span>
{items}
</div>
);
}
}
export default DropZone;
/*
NOTE 1
Commenting in this line shows that throttling works but preventDefault does not and we cannot drag and drop any box to another location.
NOTE 2
This skips throttling altogether but preventDefault does work which allows the box to be moved to a different area. Because throttling is disabled here, onDragOver fires a lot and, at times, keeps the user from moving boxes around quickly.
*/
All of the sources I have consulted have effectively implemented either a debounce or throttle to capture a value and then do something with that value but none of them have tried calling preventDefault after persist as I am attempting to do. Some of these sources are the following:
Blog post on throttling and debouncing
Example of throttling an input by peterbe
A fiddle that does exactly what I am aiming for but it is not written in React
After further research and experimentation, I found out how to resolve this.
TL;DR
My theory that event.persist() somehow prevented event.preventDefault() from working as expected was incorrect.
THE REAL PROBLEM
The reason that my drag-and-drop app did not work with throttling was because event.persist() does not forward the event to another handler but simply makes it available for other handlers to access. This means that event.preventDefault() must be called on each handler that uses the event. This seems very obvious now that I say it but because I had to send the event through multiple handlers to implement throttling, I mistakenly thought I was passing the event from one to the other.
DOCUMENTATION
What I state above is my observation and does not come from React's official documentation. But the React docs do say this:
If you want to access the event properties in an asynchronous way, you
should call event.persist() on the event, which will remove the
synthetic event from the pool and allow references to the event to be
retained by user code.
Although I had read this before, I skipped over it because I did not consider what I was doing to be asynchronous. But the answer is still here– it allows references to the event to be retained by user code.
LEARN MORE
For those who want to dig into this deeper, be sure to look at the README in my StackBlitz where I provide further details.
Related
To give a bit of context, I am conducting a research focused on digital marketing and user experience. To enable the research, it is essential that I am able to get event logs from every component in an UI so I am, then, able to create datasets of usability pattern.
To do so in a web interface, e.g. using JavaScript, that's very simple.
window.addEventListener("someEvent", (e) => {//do something with the data});
The e element gives me everything I need, and If I want to listen to all window events, I can run a for loop through the window object events and add an event listener to each. My issue is with mobile development. For cross application reasons, am I am using React Native to try to create the same effect as window.addEventListener to mobile apps.
This is my first time using React Native. After a bit of searching, I am now aware that React Native does not have a window object (at least not as we understand window in JavaScript) and that the interface is translated to the platform native components, so document.getElementBy... would't work either.
What I though of was refs. I would only need to add a reference to the top component App. So what I have working so far:
export default function App() {
const viewRef = useRef();
useEffect(() => {
//I can use ref here to iterate through all events of View and
//bind event listeners to it
}, [viewRef]);
return (
<View
ref={viewRef}
style={styles.container}
onTouchEnd={(e) => {
console.log(e.target);
}}
>
<DummyComponent />
</View>
);
}
onTouchEnd event is bind to the top-layer component, so I can get everything that is a child of it. In that useEffect, I can do the same thing I would with JavaScript's window.
So I guess this is one way to do it. However, in my research I would like to enable any React Native app to begin logging events seamlessly. The state of the art would be creating a dependency that would being logging everything simply by installing it. That said, how can I iterate a React Native application to find Views and bind their events, without need to add ANYTHING to the actual component?
In JavaScript it would be something like:
document.getElementsByTagName("View").map((view) => {//bind view events});
So I'm not sure if this can help, but you can change the defaultProps of a component on start of your application.
So using your code as example you could do something like this:
const listener = e => {
console.log(e);
}
View.defaultProps = {
...View.defaultProps, // maintains original default props
onTouchEnd: listener
};
Basically this way you can have a global listener for each View component
Does the upcoming concurrent-mode break the old guarantee, that setState updates within a click handler are flushed synchronously at the event boundary?
If i have e.g. a button, that should only ever be pressed once, a supposedly working pattern was to "just set the state to disabled in the click handler":
let counter = 0;
const C = () => {
const [disabled, setDisabled] = React.useState(false);
const handler = React.useCallback(
() => { setDisabled(true); counter++; },
[], // setDisabled is guaranteed to never change
);
return (<button onClick={handler} disabled={disabled}>click me</button>);
};
// Assert: `counter` can never be made >1 by clicking the button with one C
This pattern used to be guaranteed to work (at least given that setting the disabled-attribute prevents any further click events, which seems to be the case). The biggest related question i could find discusses this, and also shows a more or less obvious alternative (and easier to prove it works), of using a ref (unlike the answer in the linked question, maybe rather a boolean ref, but same idea, it's always sync).
Side questions: Is this information up-to-date, or did something change? It's more than three years old after all. It mentions "interactive events (such as clicks)", what are the others?
However, in concurrent-mode, rendering can be paused, which i interpret as "the js thread will be released", to allow potential key presses or whatever events to trickle in, and in that phase, additional click events could also happen, before the next render disables the button. Is therefore the way to go to use some kind of ref, or maybe explicitly adding ReactDOM.flushSync?
My current understanding of how concurrent mode works is this:
1 - a re-render starts
2 - hooks are called, they change internal state
3a - re-render is suspended
4a - internal state changes are rolled back
OR
3b - re-render is not suspended
4b - internal state changes are commited
useCallback is a thin wrapper over useMemo and uses "internal state" to save the cached value. (4a) is the key here, and from what I understand your solution is not guaranteed to work anymore.
The useRef (with a boolean flag value) solution has the same issue too because you're not guaranteed that the new value of the ref is actually going to be "commited" when re-rendering is suspended.
The useRef solution where you keep a ref to the DOM button element and directly manipulate the disabled attribute will still work even in concurrent mode. React has no way of blocking you from directly manipulating DOM.
"suspending" means reverting "internal state" + not applying the generated DOM manipulations, does not mean any side effects (like manipulating DOM directly) can be affected.
flushSync will not help either, it simply forces re-renders, does not guarantee that the current render won't be suspended.
As far as I know the setState call was always async, and you never had a way to warranty that the button will be disabled right after the click. Also there is no such thing as concurrency in JS, it has single thread, the problem is that the render can happen latter than you expect so you can receive another click until React made re-render for you.
If you need to fire the logic only once I would advice to use useRef hook and when you need to make sure that we have not clicked the button just check the value.
const isDisabled = useRef(false);
const onClick = () => {
if (!isDisabled.current) {
isDisabled.current = true;
}
}
I'm presently learning React-Testing-Library.
I'd like to test mouse interaction with an element. Presently it's a bit unclear to me the difference between userEvent.click(element) and fireEvent.click(element). Are both recommended for use, and in the example below are they being correctly implemented?
const mockFunction = jest.fn(() => console.info('button clicked'));
const { getByTestId } = render(<MyAwesomeButton onClick={mockFunction} />);
const myAwesomeButton = getByTestId('my-awesome-button');
// Solution A
fireEvent(myAwesomeButton)
expect(mockFunction.toHaveBeenCalledTimes(1);
// Solution B
userEvent.click(myAwesomeButton);
expect(mockFunction).toHaveBeenCalledTimes(1);
Thanks in advance for any clarity.
Behind the scenes, userEvent uses the fireEvent. You can consider fireEvent being the low-level api, while userEvent sets a flow of actions.
Here is the code for userEvent.click
You can see that depending of which element you are trying to click, userEvent will do a set of different actions (e.g. if it's a label or a checkbox).
According to Docs, you should use user-event to test interaction with your components.
fireEvent dispatches exactly the events you tell it to and just those - even if those exact events never had been dispatched in a real interaction in a browser.
User-event on the other hand dispatches the events like they would happen if a user interacted with the document. That might lead to the same events you previously dispatched per fireEvent directly, but it also might catch bugs that make it impossible for a user to trigger said events.
another good to mention difference between fireEvent and userEvent is that
by default fireEvent is wrapped inside act function and this is useful when the user does some action and this action will cause component updates and re-render. on the controversy, if we used userEvent probably will notice "not wrapped in act(...)" warning error that appears in the console.
act(() => {
userEvent.type(input, 'inputValue')
})
and here we don't need the act function, cause it's already wrapped over fireEvent
fireEvent.change(input, {target: {value: 'inputValue'}})
and this great article demonstrates this concept
Common mistakes with React Testing Library
The useEventListener hook and the useKeyPress seem to have a slightly different implementation, but I'm trying to figure out which is a better tool to use for my specific use case.
I have a custom dropdown select menu, and I want to listen to the arrow down, up and enter keys. My problem, or rather question, about the useKeyPress hook, is that there are two renders that happen + I'm not really sure why there is an intermediate useState.
For instance, using the useKeyPress hook, if a user click on a down arrow, the event listeners fire off twice, one would return true, and immediately return false onKeyUp:
useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, []);
Also, I'm not sure why it's doing,
const [keyPressed, setKeyPressed] = useState(false);
I'm just looking for some clarification on the difference between these two, and which one to use for my use case.
React works by updating state. If the useKeyPress hook didn't update some internal state then it would be incapable of returning an updated value reference and trigger a component rerender. useKeyPress listens for the keyDown event to toggle state to true, then listens for the keyUp event to clear that state (you don't want it stuck true.
The useEventListener is certainly a heavier tool; it's capable of monitoring many more events. It doesn't use internal state though, but rather requires callback handlers to be passed to it to be invoked when registered event occurs.
For a lightweight utility the onKeyPress is useful if you just need to trigger an effect callback in a component, but for larger projects or where you want more than to know a keyPress occurred, useEventListener can handle it.
Performance shouldn't be your primary concern as both are fast enough for nearly every use case they cover.
useKeyPress is probably not well suited for your use case as it only gives you information of when a key is pressed. It doesn't let you call a handler. It internally keeps track of the pressed state itself. But you likely want to call a handler, e.g. to change the currently selected item in your dropdown list.
Also, I'm not sure why it's doing,
const [keyPressed, setKeyPressed] = useState(false);
That's simply the use case. It keeps track of the pressed state of a button.
useEventListener is a more generic tool that lets you do the updating and state management yourself. It lets you decide what to update based on the subscribed events. You could in fact rewrite useKeyPress to internally use useEventListener.
I have a custom input validation component that I use in a form. Something like 15 instances of this component around the app. It has a beforeDestroy method in which I unsubscribe from global event called triggerGlobalValidation which triggers validation before I send request to server. As expected it's triggered only once inside this certain component.
There is a container with v-if parameter which contains one instance of the component. So when v-if="false" I expect this certain component to unsubscribe from event and get destroyed. It goes well accept for one thing: somehow this component unsubscribes ALL other instances of it from the triggerGlobalValidation event as well.
I've tested the behavior with v-show and it works as expected - all other instances keep subscribed, but since the v-show field is required for the form it's blocking validation even without being shown in the DOM. I also tested above mentioned components behavior by removing the this.$root.$off("triggerGlobalValidation") and it also works as expected + polluting the global root.
Vue documentation on $off method is saying:
If no arguments are provided, remove all event listeners;
If only the event is provided, remove all listeners for that event;
If both event and callback are given, remove the listener for that
specific callback only.
Is it possible to somehow mention in the callback, that this $off method shouldn't unsubscribe all of its instances from the event, but just this certain one being destroyed?
Check it out in codesandbox
As answered in the issue, you need to save the handler and pass it again to $off
mounted() {
this.fn = () => {
this.toggleWarning();
}
this.$root.$on("triggerChildComponents", this.fn);
},
beforeDestroy() {
this.$root.$off("triggerChildComponents", this.fn);
},