I've a state named modal in my React App. The initial value is an object that says {show: false, photo: null}.
I've two buttons in the page. One is calling the close function and another is calling the open function. open is setting the state to {show: true, photo: true} and close is just logging modal
I also wrote some code to call the close function when the Esc button is clicked.
Here's my code:
function App() {
const [modal, setModal] = useState({ show: false, photo: null });
// open func
function open() {
setModal({ show: true, photo: true });
}
// close func
function close() {
console.log(modal);
}
// function for esc key press
function escFunc(event) {
if (event.key === `Escape`) {
close();
}
}
useEffect(() => {
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, []);
return (
<>
<button onClick={open}>open</button>
<br />
<button onClick={close}>close</button>
</>
);
}
so now when I click the open button and then click the close button, it's logging {show: true, photo: true} (as expected). but the problem comes in if I press Esc now. It should log {show: true, photo: true} (as the state is already updated by the open function), but it's logging {show: false, photo: null} as if the state hasn't changed yet
Why is it happening?
Whenever a component rerenders, the entire function is reran.
In your useEffect, which is only called on the first render, you call document.addEventListener with the callback function escFunc. This escFunc has a closure that stores the value of modal, which is a reference to the original object state { show: false, photo: null }.
In your open function, you set the state to { show: true, photo: true } using the object literal syntax, which creates a whole new object with a new reference location.
The event listener is still tracking the original object.
To be able to get the new state reference, you need to remove the old event listener and then add a new event listener.
There are multiple ways to do this.
useEffect(() => {
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, [modal]); // add modal to dep array
useEffect(() => {
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, [escFunc]); // add escFunc to dep array, but this would run every render
Stylistically, this is the best option because it properly shows dependencies and doesn't have extra rerenders, but the calls to useCallback might make it slower
const close = useCallback(function() {
console.log(modal);
}, [modal]); // depends on modal
const escFunc = useCallback(function(event) {
if (event.key === `Escape`) {
close();
}
}, [close]); // depends on close
useEffect(() => {
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, [escFunc]); // add escFunc to dep array
In fact, you don't even need to have escFunc outside of useEffect if you don't use it elsewhere
const close = useCallback(function() {
console.log(modal);
}, [modal]); // depends on modal
const escFunc = useCallback(function(event) {
if (event.key === `Escape`) {
close();
}
}, [close]); // depends on close
useEffect(() => {
function escFunc(event) {
if (event.key === `Escape`) {
close();
}
}
document.addEventListener(`keydown`, escFunc, true);
return () => {
document.removeEventListener(`keydown`, escFunc, true);
};
}, [close]); // add escFunc to dep array
Related
I have a problem, I can't remove the event listener in the condition inside the callback function.
I have the impression that the syntax is correct. But the event is still active.
Does anyone know how to remove the event touchstart?
mutation.target.removeEventListener("touchstart",(e) => { handleClickButton(e) }, true);
const headerSearchBox = document.querySelector(".search-box");
function handleClickButton(event) {
event.preventDefault();
alert("You Clicked on the button")
}
const mutationCallback = function (mutationList) {
mutationList.forEach((mutation) => {
let isSearchOpen = mutation.target.classList.contains("search-container");
// If Search Bar is Open : Do This...
if (mutation.type === "attributes" && isSearchOpen && mutation.oldValue.includes("open")) {
console.log("Search Bar is Open");
mutation.target.addEventListener("touchstart",(e) => { handleClickButton(e) }, true);
} else {
console.log("Search Bar is Close");
mutation.target.removeEventListener("touchstart",(e) => { handleClickButton(e) }, true);
}
});
};
const observer = new MutationObserver(mutationCallback);
observer.observe(headerSearchBox, {
attributes: true,
attributeOldValue: true,
attributeFilter: ["class"],
});
Thanks for your support
So in a chrome extension.
when the user clicks a button
send a message to background
open a new popup window
in the new window, click on the specific element
I can get most of the way but fail to be able to focus in on the necessary document for the js to get the element ID on.
content.js
user_clicked_btn.addEventListener('click', () => {
chrome.storage.sync.get(['xyz'], (result) => {
if (result.xyz['u'].includes('123') && result.xyz['da'] !== 'cool') {
chrome.runtime.sendMessage(
{
s: 'quick',
}
);
}
});
background.js
chrome.runtime.onMessage.addListener((request) => {
if (request.s === 'quick') {
chrome.windows.create({
url: './options.html', type: "popup", focused: true
},
() => {
let queryOptions = { active: true, currentWindow: true };
chrome.tabs.query(queryOptions, function (tabs) {
tabs[0].document.getElementById("element_of_interest").click()
});
}
);
}
});
the issue could be summarized as, how do I get the "document" of a different tab? (assuming I own the 'different tab')
The solution for my problem was to add message passing the whole way to the element of interest and have it click itself.
The result looks like this and behaves as desired.
content.js
user_clicked_btn.addEventListener('click', () => {
chrome.storage.sync.get(['xyz'], (result) => {
if (result.xyz['u'].includes('123') && result.xyz['da'] !== 'cool') {
chrome.runtime.sendMessage(
{
s: 'quick',
}
);
}
});
background.js
chrome.runtime.onMessage.addListener((request) => {
if (request.s === 'quick') {
chrome.windows.create({
url: './options.html', type: "popup", focused: true
}, () => {
setTimeout(() => {
console.log("send message to options page")
chrome.runtime.sendMessage(
{
s: 'quick_click',
}
);
}, 1000);
});
}
});
in the options.html file, the desired element is a react element, so I was able to add a listener in the componentDidMount function
options/index.js
componentDidMount() {
chrome.runtime.onMessage.addListener((request) => {
if (request.s === 'quick_click') {
this.handleClick()
}
});
}
I've created a custom hook within my React app, but for some reason when I update the internal state via an event listener, it causes an infinite loop to be triggered (when it shouldn't). Here's my code:
// Note that this isn't a React component - just a regular JavaScript class.
class Player{
static #audio = new Audio();
static #listenersStarted = false;
static #listenerCallbacks = {
playing: [],
paused: [],
loaded: []
};
static mount(){
const loaded = () => {
this.removeListenerCallback("loaded", loaded);
};
this.addListenerCallback("loaded", loaded);
}
// This method is called on the initialization of the React
// app and is only called once. It's only purpose is to ensure
// that all of the listeners and their callbacks get fired.
static startListeners(){
const eventShorthands = {
playing: "play playing",
paused: "pause ended",
loaded: "loadedmetadata"
};
Object.keys(eventShorthands).forEach(key => {
const actualEvents = eventShorthands[key];
actualEvents.split(" ").forEach(actualEvent => {
this.#audio.addEventListener(actualEvent, e => {
const callbacks = this.#listenerCallbacks[key];
callbacks.forEach(callback => {
callback(e)
});
});
});
});
}
static addListenerCallback(event, callback){
const callbacks = this.#listenerCallbacks;
if(callbacks.hasOwnProperty(event)){
// Remember this console log
console.log(true);
this.#listenerCallbacks[event].push(callback);
}
}
static removeListenerCallback(event, callback){
const listenerCallbacks = this.#listenerCallbacks;
if(listenerCallbacks.hasOwnProperty(event)){
const index = listenerCallbacks[event].indexOf(callback);
this.#listenerCallbacks[event].splice(index, 1);
}
}
}
const usePlayer = (slug) => {
// State setup
const [state, setState] = useReducer(
(state, newState) => ({ ...state, ...newState }), {
mounted: false,
animationRunning: false,
allowNextFrame: false
}
);
const _handleLoadedMetadata = () => {
// If I remove this _stopAnimation, the console log mentioned
// in the player class only logs true to the console 5 times.
// Whereas if I keep it, it will log true infinitely.
_stopAnimation();
};
const _stopAnimation = () => {
setState({
allowNextFrame: false,
animationRunning: false
});
}
useEffect(() => {
Player.addListenerCallback("loaded", _handleLoadedMetadata);
return () => {
Player.removeListenerCallback("loaded", _handleLoadedMetadata);
};
}, []);
return {
mounted: state.mounted
};
};
This makes me think that the component keeps on re-rendering and calling Player.addListenerCallback(), but the strange thing is, if I put a console.log(true) within the useEffect() at the end, it'll only output it twice.
All help is appreciated, cheers.
When you're hooking (pun unintended) up inner functions in React components (or hooks) to external event handlers, you'll want to be mindful of the fact that the inner function's identity changes on every render unless you use useCallback() (which is a specialization of useMemo) to guide React to keep a reference to it between renders.
Here's a small simplification/refactoring of your code that seems to work with no infinite loops.
instead of a class with only static members, Player is a regular class of which there is an app-wide singletonesque instance.
instead of hooking up separate event listeners for each event, the often-overlooked handleEvent protocol for addEventListener is used
the hook event listener callback is now properly useCallbacked.
the hook event listener callback is responsible for looking at the event.type field to figure out what's happening.
the useEffect now properly has the ref to the callback it registers/unregisters, so if the identity of the callback does change, it gets properly re-registered.
I wasn't sure what the state in your hook was used for, so it's not here (but I'd recommend three separate state atoms instead of (ab)using useDispatch for an object state if possible).
The same code is here in a Codesandbox (with a base64-encoded example mp3 that I didn't care to add here for brevity).
const SMALL_MP3 = "https://...";
class Player {
#audio = new Audio();
#eventListeners = [];
constructor() {
["play", "playing", "pause", "ended", "loadedmetadata", "canplay"].forEach((event) => {
this.#audio.addEventListener(event, this);
});
}
play(src) {
if (!this.#audio.parentNode) {
document.body.appendChild(this.#audio);
}
this.#audio.src = src;
}
handleEvent = (event) => {
this.#eventListeners.forEach((listener) => listener(event));
};
addListenerCallback(callback) {
this.#eventListeners.push(callback);
}
removeListenerCallback(callback) {
this.#eventListeners = this.#eventListeners.filter((c) => c !== callback);
}
}
const player = new Player();
const usePlayer = (slug) => {
const eventHandler = React.useCallback(
(event) => {
console.log("slug:", slug, "event:", event.type);
},
[slug],
);
React.useEffect(() => {
player.addListenerCallback(eventHandler);
return () => player.removeListenerCallback(eventHandler);
}, [eventHandler]);
};
export default function App() {
usePlayer("floop");
const handlePlay = React.useCallback(() => {
player.play(SMALL_MP3);
}, []);
return (
<div className="App">
<button onClick={handlePlay}>Set player source</button>
</div>
);
}
The output, when one clicks on the button, is
slug: floop event: loadedmetadata
slug: floop event: canplay
I have a component where I am defining two objects with default properties, one of which is a callback method.
I am then trying to clone/assign that default object to another variable that my getter is using. Doing this, however, the callback method is never run when the button it is attached to is clicked.
Both object.assign and spread were things I attempted as well, but I may be approaching this incorrectly.
Example code:
// defaults
DEFAULT_SAVE_HANDLER = {
cb: this._modalSaveHandler,
label: "Create Case",
variant: "brand",
action: "createcase",
class: "",
disabled: true,
visible: true
};
DEFAULT_CANCEL_HANDLER = {
cb: this._modalCancelHandler,
label: "Cancel",
variant: "netrual",
class: "btn-left",
action: "close",
disabled: false,
visible: true
};
// Set vars for getter
_save = this.DEFAULT_SAVE_HANDLER;
_cancel = this.DEFAULT_CANCEL_HANDLER;
/* Spread Attempt
_save = { ... this.DEFAULT_SAVE_HANDLER };
_cancel = { ... this.DEFAULT_CANCEL_HANDLER};
*/
/* Assign Attempt
_save = Object.assign({}, this.DEFAULT_SAVE_HANDLER);
_cancel = Object.assign({}, this.DEFAULT_CANCEL_HANDLER);
*/
// Getters
get saveHandler() {
return this._save
}
set saveHandler(props){
this._save = Object.assign({}, this.DEFAULT_SAVE_HANDLER, props)
}
get cancelHandler() {
return this._cancel
}
set cancelHandler(props) {
this._cancel = Object.assign({}, this.DEFAULT_CANCEL_HANDLER, props)
}
// Callbacks
_modalSaveHandler = () => {
console.log("save handler ran");
};
_modalCancelHandler = () => {
console.log("cancel handler ran");
};
// Some random method to update a property on the button
onButtonClick(){
this.saveHandler = { disabled: false };
}
Now when my other component tries to run the callback method I am attempting to pass, it is not hitting it at all.
Is this an issue with a scope or my misunderstanding of how the properties are being cloned/referenced?
UPDATE:
When changing the default object to contain the callback function directly, it works fine.
It seems like the issue happens when trying to reference the callback method versus inline.
DEFAULT_CANCEL_HANDLER = {
//cb: this._modalCancelHandler,
cb: () => {
console.log('this works')
},
label: "Cancel",
variant: "netrual",
class: "btn-left",
action: "close",
disabled: false,
visible: true
};
You need to use a getter if you want to reference keys of the object you are just declaring.
Without the getter, the handler on the original object is already undefined, hence it's not cloned.
const obj = {
_handler() {
console.log('foo')
},
get handler() {
return this._handler
}
}
const clone = { ...obj }
obj.handler()
clone.handler()
Note, that you should not use arrow functions for this because the this keyword will not work the same when doing so.
I think you have a class where your code resides and when you create DEFAULT_OBJECTS the callback functions are undefined.
Try to move callback declaration higher than DEFAULT_SAVE_HANDLER and DEFAULT_CANCEL_HANDLER
Like:
// Callbacks
_modalSaveHandler = () => {
console.log("save handler ran");
};
_modalCancelHandler = () => {
console.log("cancel handler ran");
};
// defaults
DEFAULT_SAVE_HANDLER = {
cb: this._modalSaveHandler,
label: "Create Case",
variant: "brand",
action: "createcase",
class: "",
disabled: true,
visible: true
};
DEFAULT_CANCEL_HANDLER = {
cb: this._modalCancelHandler,
label: "Cancel",
variant: "netrual",
class: "btn-left",
action: "close",
disabled: false,
visible: true
};
//....
I have a constructor, get method, componentDidMount and componentWillUnmount methods. I want to just listen for a scroll event and, according to that, call the get method. If the scroll is at the bottom of page, the get method is called one time. That's all. The first call, that is componentDidmount(), is working one time but when I scroll down, the get method is working two times. I don't want it to execute more than once.
This is my code:
constructor(props) {
super(props);
cursor = -1
id = auth().currentUser.providerData[0].uid
this.state = {
hits: [],
isLoading: false,
over: false,
firstCall: true
};
this.get = this.get.bind(this)
this.handleScroll = this.handleScroll.bind(this)
}
get() {
this.setState({ isLoading: true });
fetch("/1.1/followers/list.json?user_id=" + id + "&cursor=" + cursor + "", {
headers: {
'Authorization': 'MyToken',
}
}).then(response => response.json())
.then(data => {
if (data.next_cursor == 0) {
this.setState({ isLoading: false })
this.setState({ over: true })
} else {
this.setState({ hits: this.state.hits.concat(data.users), isLoading: false })
cursor = data.next_cursor
console.log(data.next_cursor)
}
}).catch(error => {
return
})
}
componentDidMount() {
this.get()
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(event) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
this.get()
}
}
And that is my console output.
1634251341094964000 ----> one time
1614497980820334000 ----> two time
1579177573029464600 ----> two time
.
.
.
They are coming from console.log(data.next_cursor) at the get function.
Since it seems like the event gets fired multiple times by the window / scrollbar, you'll need to guard against duplicate calls in your code. There are many ways to do this, depending on the context and the requirements. Here are a couple options.
You could debounce the function call. This would be good if you just need to make sure it is only called once within a certain time window.
Another option is to use state, and theisLoading prop you've already defined:
get(){
if(!this.state.isLoading){
//use the setState callback param here so we don't run into async issues
this.setState({isLoading: true}, () => {
... the rest of your logic ...
//get is finished (either success or failure)
this.setState({isLoading: false});
}
}
}