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});
}
}
}
Related
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 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
i have application that calls several requests and displays that data. Everything is working, but I getting some errors that I can't figure out where is the problem..
So I have two components:
--App :Parent
---Events :Children
In App.vue calling children component:
<Events :events="gameInfo" :results="results" #startNewGame="startNewGame" />
Giving as a props "gameInfo", "results" and listening for "startNewGame" event.
When application loads first time in App.vue i'm calling function:
// Get Next Game
getNextGame() {
this.gameInfo = [];
RouletteApi.getNextGame().then(response => {
this.gameInfo.push({ response });
});
}
That children component receives and displays data.
In children component:
<script>
export default {
name: "Events",
props: ["events", "results"],
data() {
return {
GameStartTime: null,
GameId: null,
GameUUID: null
};
},
watch: {
events: function(newVal, oldVal) {
this.GameStartTime = newVal[0]["response"].fakeStartDelta--;
this.GameId = newVal[0]["response"].id;
this.GameUUID = newVal[0]["response"].uuid;
}
},
created() {
setInterval(() => {
if (this.GameStartTime > 0) {
this.GameStartTime = this.events[0]["response"].fakeStartDelta--;
} else {
this.$emit("startNewGame", this.GameUUID); -- call to parent function
}
}, 1000);
}
};
</script>
I watching, getting the data and setting timer, to execute "startNewGame" function from parent, that will make another api call and give children new data.
After timer expires I'm calling "startNewGame" function from parent:
startNewGame(uuid) {
this.startSpinning();
RouletteApi.startNewGame(uuid).then(response => {
if (response.result == null) {
setTimeout(function() {
startNewGame(uuid);
}, 1000);
} else {
this.results.push({ response });
this.gameInfo = []; -- resetting that previous dat
this.getNextGame(); -- call to first function in example
}
});
That checks if response is null then setting timeout and calling that function until response will be not null. If response came not null than I pushing to children result, resetting that gameInfo array and calling again getNextGame() function that will call request and set new value for timer in children component.
RouletteApi.js:
import axios from 'axios'
export default {
getLayout() {
return axios.get('/configuration')
.then(response => {
return response.data
})
},
getStats() {
return axios.get('/stats?limit=200')
.then(response => {
return response.data
})
},
getNextGame() {
return axios.get('/nextGame')
.then(response => {
return response.data
})
},
startNewGame(uuid) {
return axios.get('/game/' + uuid)
.then(response => {
return response.data
})
}
}
Errors:
Error in callback for watcher "events": "TypeError: Cannot read property 'response' of undefined"
TypeError: Cannot read property 'response' of undefined
at VueComponent.events (Events.vue?5cf3:30)
Uncaught ReferenceError: startNewGame is not defined
First two errors i'm getting from children component in "watch" part.
Last one when calling function in setInterval in parent component.
It looks like the watcher is running before the api call finished. Console log the new value to see what your get. Try to check if the newVal is not null or an empty array and then set the values.
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
}
},
I have a Vue component that has a vue-switch element. When the component is loaded, the switch has to be set to ON or OFF depending on the data. This is currently happening within the 'mounted()' method. Then, when the switch is toggled, it needs to make an API call that will tell the database the new state. This is currently happening in the 'watch' method.
The problem is that because I am 'watching' the switch, the API call runs when the data gets set on mount. So if it's set to ON and you navigate to the component, the mounted() method sets the switch to ON but it ALSO calls the toggle API method which turns it off. Therefore the view says it's on but the data says it's off.
I have tried to change the API event so that it happens on a click method, but this doesn't work as it doesn't recognize a click and the function never runs.
How do I make it so that the API call is only made when the switch is clicked?
HTML
<switcher size="lg" color="green" open-name="ON" close-name="OFF" v-model="toggle"></switcher>
VUE
data: function() {
return {
toggle: false,
noAvailalableMonitoring: false
}
},
computed: {
report() { return this.$store.getters.currentReport },
isBeingMonitored() { return this.$store.getters.isBeingMonitored },
availableMonitoring() { return this.$store.getters.checkAvailableMonitoring }
},
mounted() {
this.toggle = this.isBeingMonitored;
},
watch: {
toggle: function() {
if(this.availableMonitoring) {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
})
} else {
this.toggle = false;
this.noAvailalableMonitoring = true;
}
}
}
I would recommend using a 2-way computed property for your model (Vue 2).
Attempted to update code here, but obvs not tested without your Vuex setup.
For reference, please see Two-Way Computed Property
data: function(){
return {
noAvailableMonitoring: false
}
},
computed: {
report() { return this.$store.getters.currentReport },
isBeingMonitored() { return this.$store.getters.isBeingMonitored },
availableMonitoring() { return this.$store.getters.checkAvailableMonitoring },
toggle: {
get() {
return this.$store.getters.getToggle;
},
set() {
if(this.availableMonitoring) {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
});
} else {
this.$store.commit('setToggle', false);
this.noAvailableMonitoring = true;
}
}
}
}
Instead of having a watch, create a new computed named clickToggle. Its get function returns toggle, its set function does what you're doing in your watch (as well as, ultimately, setting toggle). Your mounted can adjust toggle with impunity. Only changes to clickToggle will do the other stuff.