How to use stopPropagation() with Next.js? - javascript

I am trying to use stopPropagation so my dropdown menu will close after I click on any other elements.
When I try to use stopPropagation & updating a boolean state the button re-render the page (which reset the state back to 'false') than updating the state to true and it stuck on "true" no matter what.
I wrote a code for an example:
import { useEffect, useState } from "react";
export default function IndexPage() {
const [state, setState] = useState(false);
useEffect(() => {
document.body.addEventListener("click", () => {
setState(false);
});
});
const onButtonClick = (e) => {
e.stopPropagation();
setState(!state);
};
console.log(state);
return (
<div>
<button onClick={onButtonClick}>Click</button>
<h1>{state ? "true" : "false"}</h1>
</div>
);
}
This problem seems to appear only with Next.js
I wrote the same code with React app and there is no problem.
Next.js codesandbox link
React codesandbox link
Edit:
When you fork the Next.js sandbox project everything is working fine. (and yet it doesn't solve the problem with my original code).
Anyone knows why is it happening?

This is a strange one. In the codesandbox container the state is getting preserved for overtime edits and not causing any re-renders as well so probably your e.stopPropagation when added to event handler didn't trigger a re-render and so the old handler for the button is still attached which means that the event listener on the body will keep on triggering as you click the button since our handler never got updated to factor in e.stopPropagation.
When I fork your Next.js setup, everything works fine. Because it's not an incremental change now. The container is rebuilt for me and doesn't preserve any past state and so no stale event handlers or anything.
The best way to test the difference between both setup is to see if the console logs any value when you remove the e.stopPropagation line from both. You will see that Next.js doesn't but the React one does. That means React's state even though preserved still triggered a render and so on new render the event handler that got created knows that :-
Hey, I don't have e.stopPropagation anymore so my
event will bubble up
Update - It seems that hitting Save after each change is necessary with Next.js container for a re-render. Don't rely on edits alone like in case of React container.

I did found a fix for this bug and the first person Who comment me solved this issue.
I add to my code a cleanup for my event listener and it seems to solve the problem.
It seems like cleanup inside useEffect is not just a good practice but a "must".
Because of the cleanup useEffect re-render the page twice and not three times and it prevent it from another render which cause the problem.
Big thanks for anyone tried to help.
The fixed code is:
export default function IndexPage() {
const [state, setState] = useState(false);
useEffect(() => {
window.addEventListener("click", () => setState(false));
return () => window.removeEventListener("click", () => setState(false));
});
const onButtonClick = (e) => {
e.stopPropagation();
setState(!state);
};
console.log(state);
return (
<div>
<button onClick={onButtonClick}>Click</button>
<h1>{state ? "true" : "false"}</h1>
</div>
);
}
Next.js sandbox link

Related

How can I focus a button on click in ReactJS

I have the following problem, in my web site build with nextJS and ReactJS with typescript I have products that are loaded when a button is clicked, when I click the button the items appeared and the button is scrolled down, which is the asked behavior, but when I scroll to the bottom of the page and I try to click the button the scroll remains on the same position and the items are loaded but cannot be seen, my logic is to use onFocus on the current button and when I click it to change the scroll to him, that will solve the problem when the user has scrolled down to the bottom of the page, that way it will not remain on the bottom but rather it will automatically scroll up to the button and will see the new items loaded.
The problem is that the logic to load the products are in a different component in which I am reusing the current button and right prop I am sending function to the onClick on the button. My question is how can I use onFocus. Does it has to be in the child component inside the function or in the button component. I tried to make it work on the Button component, but it doesn't work. So I am stuck for the last 4 hours and I really need a push. I would be glad if you could shine some enlargement
Here I will enter the function in the parent component for the onClick prop :
const handleLoadMoreProducts = () => {
if (!isSearchPage) {
const mappedBreadcrumbs: string[] = categoryData.breadcrumbs.map(
(crumb: BreadCrumItem) => crumb.name
);
gtmBreadcrumbTrack(mappedBreadcrumbs);
}
<LoadMoreProducts handleLoadMoreProducts={handleLoadMoreProducts} />
And here is the component that uses the Button:
interface LoadMoreProductsProps {
handleLoadMoreProducts?: (MouseEvent: React.MouseEvent<HTMLButtonElement>) => void;
Focus?: (MouseEvent: React.MouseEvent<HTMLButtonElement>) => void;
}
const LoadMoreProducts: FC<LoadMoreProductsProps> = ({ handleLoadMoreProducts }) => (
<div className="LoadMoreProducts">
<Button type="button" importance="ghost" onClick={handleLoadMoreProducts}>
Load more products
</Button>
</div>
);
I think what you want to do is to forward the ref of the element you are trying to focus in the Button component using React.forwardRef and combine it with the useImperativeHandle hook in order to gain the ability to trigger the focus with the ref outside of the Button component.
You could create a ref for the element you are trying to focus and call the focus() function for the ref on click.
More information regarding forwarding refs and the useImperativeHandle hook.

What am I doing wrong with removeEventListener?

I'm using react useEffect hook to create a dropdown component, and added an event listener to close the drop down when the user click out side the drop down, but also have created a button to hide the whole dropdown, in that case the component wouldn't exist after clicking that button so automatically the ref attached to it will equal to null, so I had to clean up by removing the event listener when the drop down is hidden.
The dropdown work well but when I click on the button that hide it it give me an error that says
TypeError: Cannot read property 'contains' of null
I guess the problem is in the clean up function that remove the event listener not working when the user click the button to hide the dropdown entirely, the ref.current was supposed to be cleaned up when the component get hidden because it will be equal to null.
useEffect(() => {
const onBodyClick = (event) => {
if (ref.current.contains(event.target)) {
return;
}
setopen(false);
};
document.body.addEventListener("click", onBodyClick);
return () => {
document.body.removeEventListener("click", onBodyClick);
};
}, []);
:v you invoke useRef without passing an initial value and then use useEffect so that is why it is null: https://reactjs.org/docs/hooks-reference.html#useref

How to stop JavaScript events when React component unmounts?

Background:
I have a web app built with React (v. 16.4.2 currently). It will only ever be used on a touch screen. It is composed of a ton of buttons to do things, and since it's all touch, I'm using touchstart/touchend to handle these actions.
Example:
This is a basic example of how I'm using the events. You click a button, it sets this.state.exampleRedirect to true, which in turn, makes the component re-render and then go to the new page (using react-router-dom). This is all working fine.
<button
type='button'
onTouchStart={() => this.setState({ exampleRedirect: true })}
className='o-button'>
Open modal
</button>
Issue:
I originally used onClick to handle buttons but had issues because my users have fat fingers and not a lot of tech background, and when they'd touch a button, they'd drag their finger over the button and it wouldn't fire the click. OnTouchStart fixes this problem by firing the minute any touch happens (drag, swipe, tap, etc).
The issue is with onTouchStart. A user touches the button, it quickly changes the page (using the router) and re-renders the new page. The app is fast, so this is almost instantaneous, which means that when the new page loads, the user's finger is usually still on the screen, thus firing ANOTHER touch event on whatever they're touching. This is often another routing button, so it just fires through screens until they lift their finger.
I am working around this by putting a delay on enabling buttons on each page load.
// example component
import React, { Component } from 'react';
class ExampleComponent extends Component {
state = { buttonsDisabled: true }
// after 300ms, the buttons are set to enabled (prevents touch events
// from firing when the page first loads
componentWillMount() {
timeoutId = setTimeout(() => {
this.setState({ buttonsDisabled: false });
}, 300);
}
render() {
return (
// button in render method
<button
disabled={this.state.buttonsDisabled}
type='button'
onTouchStart={() => this.setState({ exampleRedirect: true })}
className='o-button'>
Open modal
</button>
);
}
Is there a better way? Or a way to do what I'm doing, but globally so I don't have to add this jankity code in about 100 components?
Thanks!
Instead of using the onTouchStart event which is fired when a touch point is placed on the touch surface and using a timeout, which is a bit of a hack, you should make use of onTouchEnd since it will be fired when a touch point is removed from the touch surface, thereby ensuring that the mentioned case doesn't happen.
// example component
import React, { Component } from 'react';
class ExampleComponent extends Component {
state = { buttonsDisabled: true }
// after 300ms, the buttons are set to enabled (prevents touch events
// from firing when the page first loads
componentWillMount() {
timeoutId = setTimeout(() => {
this.setState({ buttonsDisabled: false });
}, 300);
}
render() {
return (
// button in render method
<button
disabled={this.state.buttonsDisabled}
type='button'
onTouchEnd={() => this.setState({ exampleRedirect: true })}
className='o-button'>
Open modal
</button>
);
}

Autofocus input on mount (Vue)- iOS

I would like to trigger the input box to focus (and thus keyboard to pop up) when a Vue component appears.
It does not to work on iOS.
I tried using Vue's example directive (here), and HTML5 autoFocus but neither worked.
I created this example in a sandbox (https://codesandbox.io/s/lw321wqkm).
FWIW, I do not believe it is a JS limitation, as I've seen example work (such as React Native Web using autoFocus- see example)
Parent component
<template>
<div>
<TextComp v-if='showText' />
<button #click='showText = !showText'> Show/hide input </button>
</div>
</template>
...
Child component
<template>
<div>
<input ref='focusMe' type='text'/>
</div>
</template>
<script>
export default {
name: 'TextComp',
mounted () {
this.$refs.focusMe.focus()
}
}
</script>
I hope you must have found a solution by now.
But I just wanted to add this for other new users like me.
mounted () {
this.$refs.focusMe.focus() // sometime this doesn't work.
}
Try adding this instead.
this.$nextTick(() => this.$refs.focusMe.focus())
For more info check this
Edit: 14/06/2022
Prashant's answer also helped me understand the nextTick in more depth.
nextTick allows you to execute code after you have changed some data and Vue.js has updated the virtual DOM based on your data change, but before the browser has rendered that change on the page.
You can create a fake input field and focus it during the click event.
<template>
<div>
<TextComp v-if='showText' />
<button #click='onShowText'> Show/hide input </button>
<input type="text" ref="dummykeyboard" style="opacity:0">
</div>
</template>
<script>
export default {
methods:{
onShowText() {
this.showText = !this.showText;
this.$refs.dummykeyboard.focus();
}
}
}
</script>
I would suggest triggering a click event on that field instead of focus
this.showText = !this.showText;
this.$nextTick(function () {
this.$refs.dummykeyboard.click();
})
I would go with this solution and it works for me. Just add set timeout and put focus method inside it
<template>
<div>
<input ref='focusMe' type='text'/>
</div>
</template>
<script>
export default {
name: 'TextComp',
mounted () {
setTimeout(() => {
this.$refs.focusMe.focus();
}, 500);
}
}
</script>
EDIT:
Ideally you can use $nextTick to wait until the DOM fully loaded, but sometimes it didn't work. So using setTimeout is just quick workaround to me.
https://v2.vuejs.org/v2/guide/custom-directive.html
In vue's official guide it says auto focus does not work in mobile safari. When the page loads, that element gains focus (note: autofocus doesn’t work on mobile Safari).
In iOS mobile safari, focus() only works when responding to a user interaction, like a click event. See: Mobile Safari: Javascript focus() method on inputfield only works with click?
My guess why it does not work in Vue:
In your Vue example, when you click on the button, it merely inserts a watcher into a batcher queue. A watcher has the information about what needs to update. You can see it as an update action, or update event. And later (almost immediately, at the next tick), Vue reads it (watcher) from the queue, and update the virtual dom / dom subsequently. However, this means, your code focus() is not "inside" a click event handler, rather, it is executed after the click event handler finishes.
I don't know the internal implementation of React though, so cannot explain why it works in the React example.
Still focus doesn't works for you???
Here is the solution :
setTimeout(() => {
this.$refs["input-0"].focus();
}, 1000);
The trick is to use setTimeout.

Stop click on child calling method on parent - React/JavaScript

I have a modal in React. When you click the background of the modal, the modal should close. The way I have it set up right now, if you click inside* the modal, it closes as well. Because the modal is inside the background>
handleClose(e) {
e.stopPropagation();
this.props.history.push('/business/dashboard')
}
render() {
return (
<Background onClick={e => this.handleClose(e)} name="BACKGROUND">
<Container onClick={console.log("IT CLICKED")} to={'/business/dashboard'} name="CONTAINER">
....
When I click on Container, the onClick event for Background gets called. I don't want this to happen. This is a form that users will be clicking on all the time. I need the modal to only close when you click outside the modal on Background.
I think it will work if you use stopPropagation on the Container click event instead of the Background. Just make sure that you use the onClick prop in your Container component.
class App extends React.Component {
handleClose = (e) => {
e.stopPropagation();
this.props.history.push("/business/dashboard");
};
render() {
return (
<Background onClick={this.handleClose} name="BACKGROUND">
<Container
onClick={e => e.stopPropagation()}
to={"/business/dashboard"}
name="CONTAINER"
/>
</Background>
);
}
}
EDIT: On rereading the question, the other answer is a simpler solution in this case.
The behavior you want to achieve is generally referred to as an "outside click" handler. There are a couple of decent libraries to handle this [0] and their source is pretty short and readable if you want to know how it works in detail. [1]
The general idea is to register a click event handler on the document in a HOC and check whether the event.target originates inside a React ref via Element.contains browser functionality. If is is, the handler will not be executed.
[0] https://github.com/tj/react-click-outside
[1] https://github.com/tj/react-click-outside/blob/master/index.js

Categories