React onTransitionEnd event gets called frequently - reactjs

I have React code like this:
handleTransitionEnd() {
console.log('ended');
}
<div onTransitionEnd={(ev) => this.handleTransitionEnd(ev)}....
The peculiar thing is that my logs are filling up with ended. Even if I don't have any transition css logic tied to the element at all. It seems to be firing on every state change or re render. Is this normal? I would expect it to only fire when a css transition ends.
Is there some other way this callback should be achieved?
Thanks
UPDATE:
here is a sandbox showing some strangeness: https://codesandbox.io/s/vy5wwyq5v3
Clicking the first button causes the callback to be called 3 times, then if you click the second button it gets called another 2 times even tho a transition doesn't happen. My app is even more extreme than this with it getting called a lot more often.

The css transition being used transition: "all 3s" is causing transitions on multiple properties, not just margin-left.
In the example provided, outline-color and outline-width were transitioned as well. They were then transitioned again when clicking anywhere else (not just on the second button).
You can see this by checking the event:
onTransitionEnd={e => {
e.persist(); // see: https://reactjs.org/docs/events.html#event-pooling
console.log(e.propertyName);
}}
The css transition could be more specific: transition: "margin-left 3s" to avoid this.
Alternatively, test e.propertyName for the desired case.

To add on to what dabs said in his answer, the transitionend event bubbles. This means if any children of your element have transitions on them, they will trigger the transitionend event on themselves, then bubble up to their parent, triggering on each of them, including the current element that you have the listener on.
A simple way to check that you're only running your logic for the element your listener is on is to compare e.target and e.currentTarget like so:
onTransitionEnd={e => {
if ( e.target === e.currentTarget ) {
// your logic here
}}
e.target is the element that starts the event, which can be one of the children, for example, not just the element with the listener on it.
e.currentTarget however always points to the element that has the listener on it, regardless of if it initiated the event or not.
By comparing the two, you can check that you're only running logic when it's the element with the listener on it that initiated it.

Related

Scroll to props.location.hash using useEffect after component mounts

I am creating a glossary page where each term has it's own card that is not expanded by default. Each card uses the term as it's ID because they will all be unique. I want to support direct links to a specific term via URL hash.
So, if the URL is localhost:3000/#/glossary#Actor, the initial load will scroll to the term and 'open' the card by simulating the click on the element (click handler on that element opens the card).
It is working, but too frequently. Every time I enter text into the search bar or cause a render in any way, it re-scrolls and does the card open animation again. I just want to initially scroll to the term if there is a hash, then ignore it unless it changes.
It only works if I don't include a dep array. If I include a dep array with props.location.hash, the el returns as null and nothing happens. I'm aware that the restarting of the effect is happening because of no dep array, but I don't know why it doesn't work when I add it.
useEffect(() => {
//looks for hash in URL; gets element associated with the hash
let el = document.getElementById(decodeURI(props.location.hash.slice(1)));
console.log(el)
//if element exists, get coords and offset for header before scrolling
if (el) {
const yCoordinate = el.getBoundingClientRect().top + window.pageYOffset;
const yOffset = -80;
window.scrollTo({
top: yCoordinate + yOffset,
behavior: 'smooth'
});
//opens the card only if not already open
if (!el.classList.contains('openTermCard')) {
el.click();
}
}
})
useEffect may not be the correct method. I might not even need a hook for this, idk. I just want this initial scroll event to happen on page load and not again.
It's a bit hard to figure out a solution without a working sandbox.
From your description, it seems to me your component is rendering more than once on the first load. If you put [] as dependency array, the effect only runs once, when the element is not defined yet (hence why you get null) and that's why it doesn't work.
One way to go about it is to think what else you can do in terms of designing your app. You mention that every time your component updates it focuses the card again, but that's kind of what you expect when you're using useEffect. Is it possible to re-structure the component so that the parent (the one with this side effect) doesn't re-render as often?
If you can provide a code sandbox I'm sure I (and probably others) will be able to help you further.

How to create my own onChangeComplete function for input type color on React

With react-color https://casesandberg.github.io/react-color/ .
I can use ready-made onChangeComplete function from react-color.
But I wonder how can I create that onChangeComplete by using input type color tag.
I tried onBlur but the color won't change until user clicks or presses tab
On the other hand, using onChange keep firing updates.
Because currently I'm using redux state, so dispatching update continuously when I drag and choose color isn't a good way.
Any ideas how to create onChangeComplete?
It depends on how you'd like to define a change. To prevent continuous updates every time the mouse moves, you'll probably want to update Redux state only when the mouse stops moving. (This seems to be the behaviour on the page you linked to).
You could use the following JavaScript to detect when the mouse stops moving:
let timer;
window.addEventListener('mousemove', function (e) {
console.log('Mouse moving');
clearTimeout(timer);
timer = setTimeout(() => console.log('Mouse stopped'), 300);
});
Then, try putting the above code inside your ComponentDidMount() method and replace console.log('Mouse stopped') with whatever Redux action you want to dispatch!
300 is the number of milliseconds without movement that will trigger the action, which could be changed depending on how sensitive you want your app to feel.
Lastly, remember to remove the event listener in your ComponentWillUnmount() method.
https://github.com/casesandberg/react-color/blob/7ee60d845e5d5e11e4f6ecb895b2d9755c59d09d/src/components/common/ColorWrap.js#L30
Here is the code that how react-color implemented onChangeComplete.
It is hard coded using debounce.
I put the link there for anyone interested in using that solution

Trying to trigger a WheelEvent programmatically with Hammerjs

I am using a library (https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu) that scrolls on use of the mousewheel, and I want to use this functionality when swiping left or right.
I am using hammerjs to replicate swipeleft and swiperight behavior, and this is working.
However, creating a WheelEvent does not seem to trigger the functionality dependent on the WheelEvent.
I am using componentDidUpdate for now as my react lifecycle method because for some reason this.containerRef.current is always null in componentDidMount, but once I figure out the reason behind that, I'll probably move it.
Anyway, here's my code:
componentDidUpdate() {
if(this.containerRef.current !== null) {
this.hammer = Hammer(this.containerRef.current)
this.hammer.on('swiperight', () => alert("swipe right"));
var wheelevent = new WheelEvent("wheel", {deltaX: 500, deltaY: 500});
this.hammer.on('swiperight', () => window.dispatchEvent(wheelevent));
}
}
Now I want to point out, the alert for swipe right DOES happen, so the behavior is definitely triggering, however my WheelEvent is not being caught by the scroll library.
How should I trigger a WheelEvent programmatically?
EDIT - I made a codepen about it:
https://codesandbox.io/s/react-horizontal-scrolling-menu-fi7tv
My hunch is that issue is related to Dragging being disabled and the event is canceled.
So you need to send the event down the chain a bit. I have updated the codesandbox below which works
https://codesandbox.io/s/react-horizontal-scrolling-menu-j46l8
The updated code part is below
var elem = document.getElementsByClassName("menu-wrapper")[0];
this.hammer.on("swiperight", () => elem.dispatchEvent(wheeleventRight));
this.hammer.on("swipeleft", () => elem.dispatchEvent(wheeleventLeft));
You may want to better the approach though in a more reactive fashion later. But this does show that once you sent the event in lower order elements the wheeling does work well

Force React to rerender quickly on some visually important state changes

I need to propagate state changes to user screen as quickly as possible for some important UI elements, defer other element renderring a bit.
I know about setState's callback, it doesn't help me here.
I think fiber priorities could help me, but I don't know how to use them.
Example:
I have a button that must be disabled immediately after click.
I also have many other slow components that change on that button click.
React batches rendering of the disabled button and other slow components together so the button does not get disabled immediately.
Current workaround is to delay other state changes, to make React immediately disable the button, and only then start to modify other components:
this.setState({ enabled: false }, () => {
this.debounce = setTimeout(() => {
this.props.onModified(value);
}, 200);
})
Is there some explicit way to tell React to be fast to render in some important state changes, without batching them?
(The problem is not only with buttons, but with immediate closing of the modal dialogs as well)
https://codesandbox.io/s/kk4o612ywr
You can use callback function of the setstate, something like this, which will ensures the rendering of the first change. so, your button will get the disabled first and then you can update your state with different operations. using timeout will not be accurate as there is fixed timing which will cause the inaccurate results.
Here is what I did:
this.setState({ enabled1: false },function(){
this.setState(prevState => ({ data: prevState.data + 1 }));
});
Demo

Is it necessary to call `unmountComponentAtNode` if the component's container is removed?

I render a React component SettingsTab within a wrapper called TeamView. Its API looks something like
class TeamView {
constructor() {
this.el = document.createElement('div');
}
render() {
ReactDOM.render(<SettingsTab/>, this.el);
return this;
}
remove() {
this.el.remove();
}
}
used something like
// to present the team view
const teamView = new TeamView();
document.body.appendChild(teamView.render().el);
// to remove the team view
teamView.remove();
And what I'm wondering is, should TeamView#remove call ReactDOM. unmountComponentAtNode(this.el) before calling this.el.remove()?
The examples I can find around the web make it seem like unmountComponentAtNode only needs to be called if the container is going to remain in the DOM; and the new portals example just removes the container, without calling unmountComponentAtNode.
But, I'm not sure if that's special because it's using a portal, and this post makes it kind of seem like it's always good practice to call unmountComponentAtNode.
Yes, it is important to call unmountComponentAtNode() because if you don't do this, none of the components below in the tree will know they have been unmounted.
User-defined components often do something in componentDidMount that creates a reference to the tree from the global environment. For example, you may add a window event handler (which isn't managed by React), a Redux store subscription, a setInterval call, etc. All of this is fine and normal as long as these bindings are removed in componentWillUnmount.
However, if you just remove the root from the DOM but never call unmountComponentAtNode, React will have no idea the components in that tree need to be unmounted. Since their componentWillUnmount never fires, those subscriptions stay, and prevent the whole tree from getting garbage collected.
So for all practical purposes you should always unmount the root if you're going to remove that container node. Otherwise you'll most likely get a memory leakā€”if not now, then later when some of your components (potentially deep in the tree, maybe even from third-party libraries) add subscriptions in their componentDidMount.
Even though you called this.el.remove(), you should still call the unmountComponentAtNode(this.el) because unmountComponentAtNode will clean up its event handlers and state, but the remove method will not.
For example, Eventhough you have clicked to remove the div, you can still call it's click event handlers:
var tap = document.querySelector('.tap');
var other = document.querySelector('.other');
tap.addEventListener('click', function(e) {
console.log(tap.getAttribute('data-name') + ' has been clicked');
tap.remove();
});
other.addEventListener('click', function(e) {
tap.click();
});
<div class="tap" data-name="tap">First Click me to remove me</div>
<div class="other">Then Click me </div>
I asked this question in the #react-internals Discord channel and received the following response:
So, this tallies with what #jiangangxiong says above: as long as we
don't keep our own references to component DOM elements
nor attach event handlers outside of React
and only need to support modern browsers
we should only need to remove the container to have the component's event handlers and state garbage collected, no need to call unmountComponentAtNode.

Resources