Cannot read properties of null ( play ) when leaving the component - javascript

<script>
import {onMount} from 'svelte'
let phrase = "Head Admin Esports Middle East organization ( es.me website) and Project Manager at Arab Esports Website."
let typedChar = "";
let index = 0;
let aud;
const typing = () => {
setInterval(async () => {
if (index < phrase.length) {
typedChar += phrase[index];
index++;
await aud.play()
} else {
clearInterval()
}
}, 20)
}
onMount(() => {
typing()
})
</script>
<audio bind:this={aud} src={typingAudio} />
Here I am showing the data in typing effect I wanted to add sound to this typing effect it works fine as I wanted but when I leave the component mid typing it shows Cannot read properties of null ( play ) error

This might be because the interval isn't cleared - there's neither an id provided in the call inside typing nor is it cleared when the component is destroyed
clearInterval() - If the parameter provided does not identify a previously established action, this method does nothing.
let interval
const typing = () => {
interval = setInterval(async () => {
if (index < phrase.length) {
typedChar += phrase[index];
index++;
await aud.play()
} else {
clearInterval(interval)
}
}, 20)
}
onMount(() => {
typing()
return () => clearInterval(interval)
})
Alternatively to onDestroy
If a function is returned from onMount, it will be called when the component is unmounted.
(playing the audio might be blocked if the user hasn't yet interacted with the page)

Related

Call React state methods from external function

I am building a React functional component that uses some state variables, and I am trying to modify some of these variables from an external function thats called on a button click event, but when I pass the reference to the state methods to this external function, all of them are undefined. What could be the cause? If I just put the exact same code within the functional component, it works perfectly as intended.
import React from "react";
import {CodeArea, Table, EmptyField, Button} from '../util/util.js'
import {Step, Load} from "./buttons.js" // The external function in question, Loadfunction
Core(props){
const registersInitial = new Array(32).fill(0);
let buttonNamesInitial = ['LOAD','play', 'step-forward', 'run-to', 'step-back','pause','halt', 'rerun', 'add'];
const [bigText, setText] = React.useState();
const [registers, setRegisters] = React.useState(registersInitial);
const [running, setRunning] = React.useState(false);
const [programCounter, setProgramCounter] = React.useState(0);
const [buttonNames, setButtonNames] = React.useState(buttonNamesInitial);
const [lines, setLines] = React.useState([]);
const getBigText = () => {
return bigText;
}
const getRunning = () =>{
return running;
}
const getButtonNames = () => {
return buttonNames;
}
//... some code here thats irrelevant
function getQuickbarContents(){
let functions = [ //Backend will come here
() => Load(setRunning, getRunning, setButtonNames, getButtonNames, setProgramCounter, setLines, getBigText), //Where Load gets called
() => console.log("code running..."),
() => console.log("stepping to next line..."),
() => console.log("running to location..."),
() => console.log("stepping..."),
() => console.log("pausing..."),
() => console.log("halting..."),
() => console.log("running again..."),
() => console.log("select widget to add...")
]
let contents = [];
let row = [];
for (let i = 0; i<9; i++){
row.push(<Button onClick ={functions[i]} className='quickbar' name={buttonNames[i]}/>);
contents.push(row);
row = [];
}
return contents
}
const divs = [];
let buttons = getQuickbarContents();
divs.push(<div key='left' className='left'><Table name='quickbar' rows='7' cols='1' fill={buttons}/> </div>);
//... some more code to push more components do divs
return divs;}
export default Core;`
Button looks like this:
function Button({onClick, className, name}){
return <button onClick={onClick} className={className} name={name}>{name}</button>
}
and Load like this:
export function Load({setRunning, getRunning, setButtonNames, getButtonNames, setProgramCounter, setLines, getBigText}){
let newButtonName;
if (!getRunning()){ // Functions errors out with getRunning() undefined
herenewButtonName = "Reload";
}
else{ //while running if user wants to reload
newButtonName = "LOAD";
}
let lines = getBigText().split(/\n/);
setLines(lines);
setProgramCounter(0);
setRunning(!getRunning());
const newButtonNames = getButtonNames().map((value, index) =>{
if (index === 0){
return (newButtonName);
}
return value;
})
setButtonNames(newButtonNames);
}
So essentially in my head the flow should be: state methods initialised -> button components created -> wait for click of a button -> update state variablesBut clearly, something goes wrong along the way.
I've tried using inspection mode debugging, which revealed that in fact all of the parameters to Load are undefined when they are evaluated.
Note, that everything works as intended if I change the code up like this, eg. just put the whole function within the React component;
//... everything same as before
function getQuickbarContents(){
let functions = [
() =>{
let newButtonName;
if (!getRunning()){ //User clicks to start running
newButtonName = "Reload";
}
else{
newButtonName = "LOAD";
}
let lines = getBigText().split(/\n/);
setLines(lines);
setProgramCounter(0);
setRunning(!getRunning());
const newButtonNames = getButtonNames().map((value, index) =>{
if (index === 0){
return (newButtonName);
}
return value;
})
setButtonNames(newButtonNames)},
() => console.log("code running..."),
() => console.log("stepping to next line..."),
() => console.log("running to location..."),
() => Step(setRegisters, registers, setProgramCounter, programCounter, lines[programCounter]),
() => console.log("pausing..."),
() => console.log("halting..."),
() => console.log("running again..."),
() => console.log("select widget to add...")
]
//...everything same as before
so consequently the error is somewhere in the way I pass parameters to Load, or maybe I'm doing something I shouldn't be doing in React. Either way I have no clue, any ideas?
Problem was in the way parameters were defined in Load, as #robin_zigmond pointed out. Fixed now.

How do I update a state each second for next n seconds

I have a state variable resendTime and magicLinkState.
I would like to setResendTime to resendTime - 1 for next 10 seconds when magicLinkState becomes sent.
I have tried the following code but this does not update resendTime every second.
useEffect(() => {
if (magicLinkState === "sent" && resendTime > 0) {
const timer = setInterval(() => setResendTime(resendTime - 1), 1000);
console.log(resendTime);
return () => clearInterval(timer);
}
}, []);
This code only updates the state for the first second. I have also tried passing dependency array but didn't worked.
How do I make it update the state so I achieve what is intended.
The general idea is correct in your code but needs some adjustments.
it's required to add the magicLinkState to the effect's dependencies otherwise it won't trigger when this gets the sent value but only if it's initially set with this value.
you should use the arrow syntax: setResendTime((prevResendTime) => prevResendTime - 1) in order to grab the correct state value each time
there is no need for a cleanup function in this effect since you want to clear the interval only after it's triggered and made 10 decreases to resendTime.
you should add some local count variable in order for the decrease to happen 10 times only and not forever
After these changes your code should look like below:
const decreaseResentTime = (prevResendTime) => prevResendTime - 1;
useEffect(() => {
let timer;
if (magicLinkState === "sent" && resendTime > 0) {
let count = 0;
timer = setInterval(() => {
if (count < 10) {
count++;
setResendTime(decreaseResentTime );
} else {
clearInterval(timer);
}
}, 1000);
}
}, [magicLinkState]);
You can find an example that demonstrates this solution in this CodeSandbox.
There are more improvements to be made here but they are based on your needs. For example if the magicLinkState changes to 'sent' and then to something else and then back to 'sent' within the 10 seconds, 2 intervals will run and decrease at a double rate.
You've just an issue of a stale closure over the initial resendTime state value. This is easily fixed by using a functional state update to correctly access the previous state value instead of whatever is closed over in the surround callback scope.
const timerRef = React.useRef();
useEffect(() => {
if (magicLinkState === "sent" && resendTime > 0) {
const timer = setInterval(() => {
setResendTime(resendTime => resendTime - 1); // <-- functional update
}, 1000);
timerRef.current = timer;
return () => clearInterval(timer);
}
}, []);
Note also that because of the closure that if you want to log the resendTime state as it updates you'll need to use a another useEffect hook with a proper dependency. This is also where you'd move the logic to check if the interval is completed. Use a React ref to store a reference to the timer id.
useEffect(() => {
if (resendTime <= 0) {
clearInterval(timerRef.current);
}
console.log(resendTime);
}, [resendTime]);
Can I use timestamps instead of intervals? this function checks every second for 10 seconds when called once
useEffect(() => {
if (magicLinkState === "sent" && resendTime > 0) {
let startTimeforTenSeconds = Date.now();
let startTimeforEachSecond = Date.now();
const nonBlockingCommand = new Promise((resolve, reject) => {
while (true) {
const currentTime = Date.now();
if (currentTime - startTimeforEachSecond >= 1000) {
startTimeforEachSecond = Date.now();
console.log("1 seconds have passed. set resend time here");
// code for resending time here
setResendTime(resendTime - 1);
console.log(resendTime );
}
if (currentTime - startTimeforTenSeconds >= 10000) {
console.log("10 seconds have passed. stop checking.");
resolve("10 seconds have passed.");
break;
}
}
});
nonBlockingCommand.then(() => console.log('function is done'));
}
}, []);
EDIT: Result screenshot:

function called from useEffect rendering too fast and breaks, works correctly after re-render

I am using React-id-swiper to load images for products. Let's say I have 5 products.
I am trying to use a function to detect when every image is successfully loaded, then when they are, set the useState 'loading' to false, which will then display the images.
I am using a variable 'counter' to count how many images there are to load. Once 'counter' is more than or equal to the amount in the list (5), set 'loading' to FALSE and load the images.
However, I am noticing that when I console log 'counter' when running it from useEffect, it ends at '0' and renders 5 times. 'Loading' also remains true.
However, if I trigger a re-render, I see 1,2,3,4,5 in the console. 'Loading' then turns to false.
I suspect this is to do with useEffect being too fast, but I can't seem to figure it out. Can somebody help?
Code:
const { products } = useContext(WPContext);
const [productsList, setProductsList] = useState(products);
const [filteredList, setFilteredList] = useState(products);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadImages(products);
}, []);
const loadImages = (allProducts) => {
let counter = 0;
setLoading(true);
const newProducts = allProducts.map((product) => {
const newImg = new Image();
newImg.src = "https://via.placeholder.com/450x630";
newImg.onload = function () {
counter += 1;
};
// At this point, if I console log 'counter', it returns as 0. If I trigger a re-render, it returns as 1,2,3,4,5
return {
...product,
acf: {
...product.acf,
product_image: {
...product.acf.product_image,
url: newImg.src,
},
},
};
});
handleLoading(newProducts, counter);
};
const handleLoading = (newProducts, counter) => {
if (counter >= filteredList.length) {
setLoading(false);
counter = 0;
setFilteredList(newProducts);
}
};
First weird thing is that you are calling the handleLoading() function before its even defined. This is probably not a problem but a weird practice.
You are also creating a new variable counter when you pass it in as an argument. You aren't actually reseting the counter that you want.
Also using onload is unnecessary and can cause weird behavior here since its not a synchronous operation but event based. As long as you set the img.src it should force it to load.
Try this:
const loadImages = (allProducts) => {
let counter = 0;
setLoading(true);
const newProducts = allProducts.map((product) => {
const newImg = new Image();
newImg.src = "https://via.placeholder.com/450x630";
counter += 1;
return { ...product, acf: { ...product.acf, product_image: { ...product.acf.product_image, url: newImg.src }}};
});
const handleLoading = (newProducts) => {
if (counter >= filteredList.length) {
setLoading(false);
counter = 0;
setFilteredList(newProducts);
}
};
handleLoading(newProducts);
};
useEffect(() => {
loadImages(products);
}, []);

Trying to call two AI functions in the render method in react but they're called asynchronously

I'm working on a board game with an AI mode in it. And now I'm trying to call the AI's move but it is called asynchronously. I don't know what I can do. I've tried a lot of stuff; calling setTimeout synchronously, calling it inside the onClick... Do you have any solutions?
Here's the code:
render() {
// datas that will be rendered regularly come here
const current = this.state.history[this.state.stepNumber]
const elemsPlayer = this.checkElementsAround(this.checkEmptySpaces())
if (this.state.ourPlayer !== (this.state.blackIsNext) ? 'black' : 'white') {
if (this.state.gameMode === "AI-Easy") {
setTimeout(()=>{
this.easyAI()
}, 1500)
} else if (this.state.gameMode === "AI-Hard") {
setTimeout(()=>{
this.minimaxAI()
}, 1500)
}
}
// Return the game scene here
return (
<div id="master-container">
<div id="gameboard-container">
<GameBoard squares={current} onClick={(row,col) => {
// Where player's move will be called
}}/>
</div>
</div>
)
}
Here's the easyAI function (minimax one is empty for now):
easyAI(){
const elemsAI = this.checkElementsAround(this.checkEmptySpaces())
const validsAI = []
for (let elAI=0;elAI<elemsAI.length;elAI++) {
const turningAI = this.checkTurningStones(elemsAI[elAI].directions, this.state.blackIsNext)
//console.log(turningAI)
if (turningAI.length !== 0) {
validsAI.push(elemsAI[elAI])
}
}
// ValidsAI returns an object {emptySquares: coordinates of the empty square, turningStones: coordinates of the stones that will be turned upside down if emptySquares is played}
//Check if AI has a move to make
if (validsAI.length !== 0) {
//Perform the move
var elementToTurnAI = null
try{
elementToTurnAI = validsAI[Math.floor(Math.random()*validsAI.length)]
} catch {
console.log(null)
}
//console.log(elementToTurnAI)
const turningAI = this.checkTurningStones(elementToTurnAI.directions, this.state.blackIsNext)
const selfAI = elementToTurnAI.coordinates
//console.log(selfAI)
//console.log(turningAI)
turningAI.unshift([selfAI[0],selfAI[1]])
// Turn the stones
const upcomingAI = this.handleMove(turningAI)
// Update the state
this.setState({
history: upcomingAI,
stepNumber: upcomingAI.length - 1
},() => {
this.setWinnerAndTurn(this.state.history)
//console.log(this.state.history)
})
}
}
If you have to take a look at something else in order to find a solution, please do let me know.
Thanks in advance :)

Push function is not working properly localStorage Javascript vanilla

I am doing a time-to-click game as you can imagine the fastest time would be the first place. I just want to have 3 scores. and have it on localstorage but every time I start a new game the actual score resets its value and it doesnt generate other scores. the 3 scores should have as value 0. I tried to push them as arrays but push function is not working well. at this point I am stuck. I dont know what to do. If u may help me, I would be really grateful, thanks!
let times = Array.from({
length: 3
})
let interval2;
// Timer CountUp
const timerCountUp = () => {
let times = 0;
let current = times;
interval2 = setInterval(() => {
times = current++
saveTimes(times)
return times
}, 1000);
}
// Saves the times to localStorage
const saveTimes = (times) => {
localStorage.setItem('times', JSON.stringify(times))
}
// Read existing notes from localStorage
const getSavedNotes = () => {
const timesJSON = localStorage.getItem('times')
try {
return timesJSON ? JSON.parse(timesJSON) : []
} catch (e) {
return []
}
}
//Button which starts the countUp
start.addEventListener('click', () => {
timerCountUp();
})
// Button which stops the countUp
document.querySelector('#start_button').addEventListener('click', (e) => {
console.log('click');
times.push(score = interval2)
getSavedTimes()
if (interval) {
clearInterval(interval);
clearInterval(interval2);
}
})
This:
// Button which stops the countUp
document.querySelector('#start_button').addEventListener('click', (e) => {
Probably should be this:
// Button which stops the countUp
document.querySelector('#stop_button').addEventListener('click', (e) => {
...considering you're already attaching an event-handler to start's 'click' event in the previous statement, and no-one would use #start_button as the id="" of a stop button.

Categories