Javascript removeEventListener on dynamic components - javascript

so my code looks like
useEffect(() => {
const element = document.getElementById('player');
document.getElementById('fullscreen').addEventListener('click', () => {
if (screenfull.isEnabled) {
screenfull.request(element);
}
});
document.getElementById('fullscreen-out').addEventListener('click', () => {
if (screenfull.isEnabled) {
screenfull.toggle(element);
}
});
return () => {
document.getElementById('fullscreen').removeEventListener('click', () => {
if (screenfull.isEnabled) {
screenfull.request(element);
}
});
document.getElementById('fullscreen-out').removeEventListener('click', () => {
if (screenfull.isEnabled) {
screenfull.toggle(element);
}
});
}
}, [])
The content platform I'm building has a master state that the admins can change at any time, some components, (such as in the example code) will not be displayed&rendered on the certain state.
Now the problem then lies in the detachment of a event listener. When the state changes, the component is then ripped out of the DOM and the event listener then cannot be removed(in my understanding).
So this causes the following error TypeError: Cannot read properties of null (reading 'removeEventListener')
How can I detach the listener when the component exits?
Any help is appreciated, thanks in advance.

As Dai pointed out in his comment, I should've let React do the work and use the event handlers the framework provides.
onClick is an event handler by itself, and does the exact same thing as the code I provided in my first example.
Thanks Dai.
function requestScreenfull() {
const element = document.getElementById('player');
if (screenfull.isEnabled) {
screenfull.request(element);
}
}
function toggleScreenfull() {
const element = document.getElementById('player');
if (screenfull.isEnabled) {
screenfull.toggle(element);
}
}
<button onClick={requestScreenfull}>Fullscreen</button>
<button onClick={toggleScreenfull}>Toggle</button>
More information on React Documentation

Related

Custom directive to replace #mouseenter + #mouseleave event?

I'm having lots of elements on which #mouseenter set a value to true and #mouseleave sets it to false. Basically what I need is a way to set a reactive variable to true if the mouse hovers the element.
I've been trying to figure out how to write such custom directive from the docs but it only mentions how to use .focus() js function on an element. Which js functions would be used for said directive?
Something like:
const vHover = {
mounted: (el) => {
el.addEventListener('mouseenter', state.hover=true)
el.addEventListener('mouseleave', state.hover=false)
}
}
I think you could do something like:
app.directive('hover', {
created(el, binding) {
const callback = binding.value
el.onmouseenter = () => callback(true)
el.onmouseleave = () => callback(false)
},
unmounted(el) {
el.onmouseenter = null
el.onmouseleave = null
}
})
Template:
<button v-hover="onHoverChange">Example</button>
Methods:
onHoverChange(isHovered) {
console.log(isHovered)
}
I believe this is not the intended use of directives. The value of the state cannot be mutated within the directive. You can pass the variable through the binding, but you cannot update it.
binding: an object containing the following properties.
value: The value passed to the directive. For example in v-my-directive="1 + 1", the value would be 2.
oldValue: The previous value, only available in beforeUpdate and updated. It is available whether or not the value has changed.
so if you do el.addEventListener('mouseenter', binding.hover=true), as you may have noticed, it will not update the state.
However, if we use the internals (PSA: though not recommended since they could potentially change at any time), you could get instance using the vnode, and use the binding.arg to denote which Proxy (state)
so you could get the reactive variable with vnode.el.__vueParentComponent.data[binding.arg]
<script>
export default {
data(){
return {
state: { hover:false }
}
},
directives: {
hover: {
mounted(el, binding, vnode) {
el.addEventListener('mouseenter', () => {
vnode.el.__vueParentComponent.data[binding.arg].hover = true
})
el.addEventListener('mouseleave', () => {
vnode.el.__vueParentComponent.data[binding.arg].hover = false
})
},
}
}
}
</script>
<template>
<h1 v-hover:state="state">HOVER {{ state }}</h1>
</template>
SFC playground link
of course you might want to add the unmounted and even consider adding mouseleave dynamically only when mouseenter fires
This is how it can be done inside the component:
const vHover = {
mounted: (el) => {
el.addEventListener('mouseenter', () => {state.hover=true})
el.addEventListener('mouseleave', () => {state.hover=false})
},
unmount: (el) => {
el.removeEventListener('mouseenter', () => {state.hover=true})
el.removeEventListener('mouseleave', () => {state.hover=false})
}
}

setState causing infinite loop in custom hook

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

Javascript method to be able see if a value changes without re rendering the page

I am trying to be able to read a value that is boolean to see if a user did a specific action or not and I am using the ReactJS functional component style. I am trying to read the runValue in my code to see if the run() method changed the value and I want to be able to read this value without recalling the function.
I want to be able to put in my useEffect method this line of code;
Run.RunFunction().run((index) => {
if (index) {
\\\ do stuff here if index is true
} else {
///if index is false
}
}
my code
const Run = {
RunFunction: () => {
let runValue = false;
return {
run() {
runValue = true
},
listener: function(val) {},
checkStatus: function(listen) {
this.listener = listen
}
}
},
}
Run.RunFunction().checkStatus((index) => {
if (index) {
console.log('running')
} else {
console.log('not running')
}
});
I am having trouble having this code to work and I want to be able to see the value of the runValue initially and if it changes.
Thank you

React window unload event doesn't fire

I need to use navigator.sendBeacon() on window unload in order to let my server know the client has closed his window. I have searched everywhere and it just doesn't work for me.
For reference, the solution in this post didn't work either.
I have an App component that wraps my entire project. I am trying to set the unload event on it's componentDidMount() lifecycle method, and it just won't fire.
componentDidMount() {
window.addEventListener("beforeunload", this.unload);
}
componentWillUnmount() {
window.addEventListener("beforeunload", this.unload);
}
unload(e) {
e.preventDefault();
e.returnValue = 'test';
navigator.sendBeacon(`http://localhost:8080/window-closed/${this.props.username}`);
return 'test';
}
I expect the server to get the AJAX call, and the window to prompt the user 'test' before the window is closed. What actually happens is the window just closes as usual.
NOTE: the return 'test' & e.returnValue = '' statements are purely for testing. I'm only interested in the AJAX request.
Any help would be much appreciated.
If you're using a functional component, you can try this:
useEffect(() => {
window.addEventListener("beforeunload", handleUnload);
return () => {
window.removeEventListener("beforeunload", handleUnload);
};
}, []);
const handleUnload = (e) => {
const message = "o/";
(e || window.event).returnValue = message; //Gecko + IE
return message;
};
You should bind this to the unload method or transform it to arrow function.
Binging way
constructor() {
super();
this.state = {
//stuff
};
this.unload.bind(this);
}
componentDidMount() {
window.addEventListener("beforeunload", this.unload);
}
componentWillUnmount() {
window.removeEventListener("beforeunload", this.unload);
}
unload(e) {
navigator.sendBeacon(`http://localhost:8080/window-closed/${this.props.username}`);
}
Arrow functions way:
constructor() {
super();
this.state = {
//stuff
};
}
componentDidMount() {
window.addEventListener("beforeunload", this.unload);
}
componentWillUnmount() {
window.removeEventListener("beforeunload", this.unload);
}
unload = (e) => {
navigator.sendBeacon(`http://localhost:8080/window-closed/${this.props.username}`);
}
Remember to remove the eventlistener on componentWillUnmount (you are currently adding it again).
You may be able to use navigator.sendBeacon.
const UnloadBeacon = ({
url,
payload = () => {},
children
}) => {
const eventHandler = () => navigator.sendBeacon(url, payload())
useEffect(() => {
window.addEventListener('unload', eventHandler, true)
return () => {
window.removeEventListener('unload', eventHandler, true)
}
}, [])
return children
}
full example here: https://gist.github.com/tmarshall/b5433a2c2acd5dbfc592bbc4dd4c519c
Have you tried declaring upload function as a fat arrow function? Also declare it before componentDidMount. (for better readability) before passing it as a reference.
Also have you tried attaching listener in contructor ? And make surw to bind your function in constructor. For reference
Also destroy the listener at componentWillUnmount, instead of adding it. (useless) use reference to listener, to destroy. Which you will create in constructor.
Best of luck
I am unsure why beforeunload is not working, but as a workaround, you may consider using the hashchange event.
componentDidMount() {
window.addEventListener("hashchange", this.doSomething, false);
}
componentWillUnmount() {
window.removeEventListener("hashchange", this.doSomething, false);
}

VueJS how to use _.debounce on data changes

I'm building a little vue.js-application where I do some post requests. I use the watch-method to whach for api changes which then updates the component if the post request is successfull. Since the watcher constantly checks the API I want to add the ._debounce method but for some reason it doesn't work.
here is the code:
<script>
import _ from 'lodash'
export default {
data () {
return {
cds: [],
cdCount: ''
}
},
watch: {
cds() {
this.fetchAll()
}
},
methods: {
fetchAll: _.debounce(() => {
this.$http.get('/api/cds')
.then(response => {
this.cds = response.body
this.cdCount = response.body.length
})
})
},
created() {
this.fetchAll();
}
}
</script>
this gives me the error: Cannot read property 'get' of undefined
Can someone maybe tell me what I'm doing wrong?
EDIT
I removed the watch-method and tried to add
updated(): {
this.fetchAll()
}
with the result that the request runs in a loop :-/ When I remove the updated-lifecycle, the component does (of course) not react to api/array changes... I'm pretty clueless
Mind the this: () => { in methods make the this reference window and not the Vue instance.
Declare using a regular function:
methods: {
fetchAll: _.debounce(function () {
this.$http.get('/api/cds/add').then(response => {
this.cds = response.body
this.cdCount = response.body.length
})
})
},
Other problems
You have a cyclic dependency.
The fetchAll method is mutating the cds property (line this.cds = response.body) and the cds() watch is calling this.fetchAll(). As you can see, this leads to an infinite loop.
Solution: Stop the cycle by removing the fetchAll call from the watcher:
watch: {
cds() {
// this.fetchAll() // remove this
}
},

Categories