I'm trying to time how long a user takes to answer a question in a quiz.
The current code looks something like this
function Answer({ currentQuestion, submitAnswer }) {
var start = Date.now();
const inputsRef = useRef();
// const [secondsRemaining, setSecondsRemaining] = useState(0)
const handleInput = (e) => {
let value = e.target.value;
let nth = parseInt(e.target.dataset.nth, 10);
if (value === "") return;
let inputs = inputsRef.current.querySelectorAll("input");
inputs.forEach((element) => {
if (parseInt(element.dataset.nth, 10) === nth + 1) {
element.focus();
element.select();
}
});
};
const handleSubmit = () => {
let finalValue = "";
const finished = Date.now()
const secondsCompleted = finished - start;
let seconds = Math.floor((secondsCompleted / 1000) % 60);
console.log( seconds)
let inputs = inputsRef.current.querySelectorAll("input");
inputs.forEach((element) => (finalValue += element.value));
finalValue = finalValue.toUpperCase();
submitAnswer(finalValue);
};
useEffect(() => {
inputsRef.current.querySelectorAll("input")[0].focus();
}, []);
return (
<div>
<div
ref={inputsRef}
className="input-container flex justify-center flex-wrap"
>
{.... <input.../>}
....
);
}
Currently the timer only begins when use inputs a value into the input form.
How can I have it start when this function renders?
You should use state for the start in your component because each time there is update inside your application, it will change and it will never be the right time the user started to see the question. And you should add useEffect to check when the question changes and update the start time.
You should make it like this:
const [start, setStart] = useState();
useEffect(() => {
setStart(Date.now());
}, [currentQuestion])
You could do something like this:
import { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
function Answer({ currentQuestion, submitAnswer }) {
const inputsRef = useRef();
const [secondsRemaining, setSecondsRemaining] = useState(0);
const handleInput = (e) => {
if (!secondsRemaining) {
setSecondsRemaining(new Date().getTime());
}
};
const handleSubmit = () => {
const time = new Date().getTime();
setSecondsRemaining(time - secondsRemaining);
};
useEffect(() => {
inputsRef.current.focus();
}, []);
console.log(secondsRemaining);
return (
<div>
<div ref={inputsRef} onChange={handleInput}>
<input />
<button type="submit" onClick={handleSubmit}>
Submit
</button>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Answer />, rootElement);
Related
I'm trying a project where I use my handpose to play the dino game in chrome. It's been 6 hours and I cannot seem to find a good solution to passing props to the game. Here is the code of the App.js
function App() {
const [isTouching, setIsTouching] = useState(false);
const webcamRef = useRef(null);
const canvasRef = useRef(null);
const runHandpose = async () => {
const net = await handpose.load();
console.log('Handpose model loaded.');
// Loop and detect hands
setInterval(() => {
detect(net);
}, 100)
};
const detect = async (net) => {
if (
typeof webcamRef.current !== 'undefined' &&
webcamRef.current !== null &&
webcamRef.current.video.readyState === 4
) {
...
await handleDistance(hand);
}
}
const handleDistance = (predictions) => {
if (predictions.length > 0) {
predictions.forEach(async (prediction) => {
const landmarks = prediction.landmarks;
const thumbTipPoint = landmarks[4]
const indexFingerTipPoint = landmarks[8]
const xDiff = thumbTipPoint[0] - indexFingerTipPoint[0]
const yDiff = thumbTipPoint[1] - indexFingerTipPoint[1]
const dist = Math.sqrt(xDiff*xDiff + yDiff*yDiff)
if (dist < 35) {
setIsTouching(true);
} else {
setIsTouching(false);
}
})
}
}
useEffect(() => {
console.log(isTouching)
}, [isTouching])
useEffect(() => {
runHandpose();
}, [])
return (
<div className="App">
<div className="App-header">
<Webcam ref={webcamRef}
style={{...}}/>
<canvas ref={canvasRef}
style={{...}} />
</div>
<Game isTouching={isTouching} />
</div>
);
}
And here is some code from the game
export default function Game({ isTouching }) {
const worldRef = useRef();
const screenRef = useRef();
const groundRef1 = useRef();
const groundRef2 = useRef();
const dinoRef = useRef();
const scoreRef = useRef();
function setPixelToWorldScale() {
...
}
function handleStart() {
lastTime = null
speedScale = 1
score = 0
setupGround(groundRef1, groundRef2)
setupDino(dinoRef, isTouching)
setupCactus()
screenRef.current.classList.add("hide")
window.requestAnimationFrame(update)
}
async function update(time) {
if (lastTime == null) {
lastTime = time
window.requestAnimationFrame((time) => {
update(time)
})
return
}
const delta = time - lastTime
updateGround(delta, speedScale, groundRef1, groundRef2)
updateDino(delta, speedScale, dinoRef)
updateCactus(delta, speedScale, worldRef)
updateSpeedScale(delta)
updateScore(delta, scoreRef)
// if (checkLose()) return handleLose(dinoRef, screenRef)
lastTime = time
window.requestAnimationFrame((time) => {
update(time)
})
}
function updateSpeedScale(delta) {...}
function updateScore(delta) {...}
function checkLose() {...}
function isCollision(rect1, rect2) {...}
useEffect(() => {
console.log(isTouching)
window.requestAnimationFrame(update);
}, [isTouching])
function handleLose() {...}
useEffect(() => {
setPixelToWorldScale()
window.addEventListener("resize", setPixelToWorldScale())
document.addEventListener("click", handleStart, { once: true })
},[])
return (...);
}
What I've been trying to do is how I can pass isTouching to the game everytime my thumb and my index finger meet. But I want to avoid re-render the game and I only want to update the dino. But I cannot find a way to do that. I was also trying to create a isJump state inside the game but I don't know how to pass the setisJump to the parent (which is App) so that I can do something like this:
useEffect(() => {
setIsJump(true)
}, [isTouching])
Does anyone have a better idea of how to do this? Or did I made a mistake on passing the props here? Thank you
I am trying to build a simple plus/minus-control in React. When clicked on either plus or minus (triggered by onMouseDown) the value should change by a defined step and when the button is held the value should in-/decrease at a specified interval after a specified delay. When the button is released (onMouseUp), the interval should stop.
The code below runs ok on onMouseDown and hold, but when I just click on the button the interval starts anyway. I see that I need to make sure that the button is still down before the interval is started, but how do I achieve that? Thank you for any insights.
let plusTimer = useRef(null);
const increment = () => {
setMyValue(prev => prev + myStep);
setTimeout(() => {
plusTimer.current = setInterval(
() => setMyValue(prev => prev + myStep),
100
);
}, 500);
};
const intervalClear = () => {
clearInterval(plusTimer.current);
};
I think I will let the code speak for itself:
const {useCallback, useEffect, useState} = React;
const CASCADE_DELAY_MS = 1000;
const CASCADE_INTERVAL_MS = 100;
function useDelayedCascadeUpdate(intervalTime, delay, step, callback) {
const [started, setStarted] = useState(false);
const [running, setRunning] = useState(false);
const update = useCallback(() => callback((count) => count + step), [
callback,
step
]);
const handler = useCallback(() => {
update();
setStarted(true);
}, [update, setStarted]);
const reset = useCallback(() => {
setStarted(false);
setRunning(false);
}, [setStarted, setRunning]);
useEffect(() => {
if (started) {
const handler = setTimeout(() => setRunning(true), delay);
return () => {
clearTimeout(handler);
};
}
}, [started, setRunning, delay]);
useEffect(() => {
if (running) {
const handler = setInterval(update, intervalTime);
return () => {
clearInterval(handler);
};
}
}, [running, update, intervalTime]);
return [handler, reset];
}
function App() {
const [count, setCount] = useState(0);
const [incrementHandler, incrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
1,
setCount
);
const [decrementHandler, decrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
-1,
setCount
);
return (
<div>
<div>{count}</div>
<button onMouseDown={incrementHandler} onMouseUp={incrementReset}>
+
</button>
<button onMouseDown={decrementHandler} onMouseUp={decrementReset}>
-
</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
I am trying to match pi value from input.
"pracInput" dependency has been used in useEffect so I can get latest value from input and check.
But the problem is that when I input some value the for loop restart.
if I input 9;
expected value=14159; counting: 5; progress width : 60 ;
if I input another value 2;
expected => value=141592; counting: 6; progress width : 72;
import React, { useEffect, useState } from "react";
const PiGame = () => {
const [pracInput, setPracInput] = useState("1415");
const pi = "141592653589793238462643";
const [widthText, setWidthText] = useState(0);
const [counting, setCounting] = useState(0);
useEffect(() => {
const runLoop2 = async () => {
for (let i = 0; i < pracInput.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (pracInput[i] === pi[i]) {
console.log(i)
console.log(true);
setWidthText((prev) => prev + 12);
setCounting((prev) => prev + 1);
} else {
console.log(false);
alert('not match')
}
}
};
runLoop2();
}, [pracInput]);
const handleChange = (e) => {
const val = e.target.value;
if (/^[0-9]+$/.test(val)) {
setPracInput(val);
}
};
return (
<div>
<div>
value: <input
type="text"
style={{
width: "80%",
marginTop: 100,
height: 25,
fontSize:25
}}
value={pracInput}
onChange={handleChange}
/>
<div style={{ fontSize: 25 }}>counting : {counting}</div>
<div style={{ backgroundColor: "green", width: widthText, height: 20 }}></div>
<div>progress width : {widthText}</div>
</div>
</div>
);
};
export default PiGame;
According to what I understood from question:
import React, { useEffect, useState } from "react";
const PiGame = () => {
const [pracInput, setPracInput] = useState("1415");
const pi = "141592653589793238462643";
useEffect(() => {
let subscribed = true;
const runLoop2 = async () => {
for (let i = 0; i < pracInput.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (subscribed) {
if (pracInput[i] === pi[i]) {
console.log(i)
console.log(true);
} else {
console.log(false);
}
}
}
};
runLoop2();
return () => subscribed = false; // to avoid memory leak on unmount of component
}, []);. // Passing empty array will make it execute only once, at component mount
const handleChange = (e) => {
const val = e.target.value;
if (/^[0-9]+$/.test(val)) {
setPracInput(val);
}
};
return (
<div>
<div>
<input
type="text"
style={{
width: "80%",
}}
value={pracInput}
onChange={handleChange}
/>
<div>{pracInput}</div>
</div>
</div>
);
};
export default PiGame;
As you are doing asynchronous task in useEffect you must use some logic/trick to avoid memory leak.
Because you added "pracInput" in the array. "UseEffect" excutes everytime you call it and therefore will always call the looping function to start over. To stop this, you could either remove the "pracInput" from the dependency array and get the value in another way or you could use the "UseState" hook to set a certain value when the loop starts and then base a condition on your loop function call.
Something like this
const [cond, setCond] = useState(false);
Set its value to be true when the loop starts then add a condition like this
if(cond == false)
runLoop2();
Are you looking for this?
import React, { useEffect, useState, useRef } from "react";
const PiGame = () => {
const [pracInput, setPracInput] = useState("1415");
const pi = "141592653589793238462643";
const counter = useRef(4); // store the counter
useEffect(() => {
const runLoop2 = async () => {
for (let i = counter.current; i < pracInput.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (pracInput[i] === pi[i]) {
console.log(i)
console.log(true);
} else {
console.log(false);
}
}
counter.current = counter.current + pracInput.length;
};
runLoop2();
}, [pracInput]);
// the rest same as before
See your for loop is present inside the useEffect which gets triggered whenever your pracInput gets changed,so everything inside that useEffect will be triggered.Your pracInput is being changed when handleChange is being called ,so that means whenevr some input will be provided and if (/^[0-9]+$/.test(val)) { setPracInput(val); } becomes true,your pracInput will change and useEffect will be triggered and hence that for loop will start.And since your for loop requires your changes of pracInput ,moving it outside of the useEffect also wont make sense.
If you want to just start your for loop once only then remove it from that useEffect and do it like this:
useEffect(()=>{
for loop goes here
},[]);
This will ensure that for loop runs just once ,that is when the component will render for the first time.
Edit: According to your output i think this should work:
const [widthText, setWidthText] = useState(0);
const [counting, setCounting] = useState(0);
useEffect(() => {
const TimeoutId = setTimeout(() => {
if(pracInput==="1415")
{
setCounting(4);
setWidthText(60);
}
else
{
setCounting(pracInput.length+1);
setWidthText(60+(pracInput.length-4)*12);
}
}, 1000);
return () => clearTimeout(TimeoutId);
}, [pracInput]);
const handleChange = (e) => {
const val = e.target.value;
if (/^[0-9]+$/.test(val)) {
setPracInput(val);
}
};
And you can now remove the for loop
So I was trying to implement a filter that is controlled by a search bar input. So I think part of the problem is that I have this filter hooked on a timer so that while the user is typing into the search bar, it isn't re-running for each letter typed in.
What it is currently doing is that after the item is typed in the search bar, the timer goes off and the filters are working but it doesn't appear that the app is re-rendering with the new filtered variable.
I suspect that it might have something to do with useEffect but I'm having trouble wrapping my head around it and it wasn't working out for whatever I was doing with it.
Here's the code:
const RecipeCards = (props) => {
const inputTypingRef = useRef(null);
let preparingElement = props.localRecipes;
let cardElement;
let elementsSorted;
const ingredientCountSort = (recipes) => {
elementsSorted = ...
}
const elementRender = (element) => {
cardElement = element.map((rec) => (
<RecipeCard
name={rec.name}
key={rec.id}
ingredients={rec.ingredients}
tags={rec.tags}
removeRecipe={() => props.onRemoveIngredients(rec.id)}
checkAvail={props.localIngredients}
/>
));
ingredientCountSort(cardElement);
};
if (inputTypingRef.current !== null) {
clearTimeout(inputTypingRef.current);
}
if (props.searchInput) {
inputTypingRef.current = setTimeout(() => {
inputTypingRef.current = null;
if (props.searchOption !== "all") {
preparingElement = props.localRecipes.filter((rec) => {
return rec[props.searchOption].includes(props.searchInput);
});
} else {
preparingElement = props.localRecipes.filter((rec) => {
return rec.includes(props.searchInput);
});
}
}, 600);
}
elementRender(preparingElement);
return (
<div className={classes.RecipeCards}>{!elementsSorted ? <BeginPrompt /> : elementsSorted}</div>
);
};
Don't worry about ingredientCountSort() function. It's a working function that just rearranges the array of JSX code.
Following up to my comment in original question. elementsSorted is changed, but it doesn't trigger a re-render because there isn't a state update.
instead of
let elementsSorted
and
elementsSorted = ...
try useState
import React, { useState } from 'react'
const RecipeCards = (props) => {
....
const [ elementsSorted, setElementsSorted ] = useState();
const ingredientCountSort = () => {
...
setElementsSorted(...whatever values elementsSorted supposed to be here)
}
Reference: https://reactjs.org/docs/hooks-state.html
I used useEffect() and an additional useRef() while restructuring the order of functions
const RecipeCards = (props) => {
//const inputTypingRef = useRef(null);
let preparingElement = props.localRecipes;
let finalElement;
const [enteredFilter, setEnteredFilter] = useState(props.searchInput);
let elementsSorted;
const [elementsFiltered, setElementsFiltered] = useState();
const refTimer = useRef();
const filterActive = useRef(false);
let cardElement;
useEffect(() => {
setEnteredFilter(props.searchInput);
console.log("updating filter");
}, [props.searchInput]);
const filterRecipes = (recipes) => {
if (enteredFilter && !filterActive.current) {
console.log("begin filtering");
if (refTimer.current !== null) {
clearTimeout(refTimer.current);
}
refTimer.current = setTimeout(() => {
refTimer.current = null;
if (props.searchOption !== "all") {
setElementsFiltered(recipes.filter((rec) => {
return rec.props[props.searchOption].includes(enteredFilter);
}))
} else {
setElementsFiltered(recipes.filter((rec) => {
return rec.props.includes(enteredFilter);
}))
}
filterActive.current = true;
console.log(elementsFiltered);
}, 600);
}else if(!enteredFilter && filterActive.current){
filterActive.current = false;
setElementsFiltered();
}
finalElement = elementsFiltered ? elementsFiltered : recipes;
};
const ingredientCountSort = (recipes) => {
console.log("sorting elements");
elementsSorted = recipes.sort((a, b) => {
...
filterRecipes(elementsSorted);
};
const elementRender = (element) => {
console.log("building JSX");
cardElement = element.map((rec) => (
<RecipeCard
name={rec.name}
key={rec.id}
ingredients={rec.ingredients}
tags={rec.tags}
removeRecipe={() => props.onRemoveIngredients(rec.id)}
checkAvail={props.localIngredients}
/>
));
ingredientCountSort(cardElement);
};
//begin render /////////////////// /// /// /// /// ///
elementRender(preparingElement);
console.log(finalElement);
return (
<div className={classes.RecipeCards}>{!finalElement[0] ? <BeginPrompt /> : finalElement}</div>
);
};
There might be redundant un-optimized code I want to remove on a brush-over in the future, but it works without continuous re-renders.
I am trying to get the textarea to be focused on so the user doesn't have to click start then click on the textarea, but keep getting an error when i click start, using codesandbox IDE.
Bit stuck now so not sure what i can do to fix it, the code runs fine when inputRef.current.focus and inputRef.current.disabled aren't included in the startGame() function.
import React, { useState, useEffect, useRef } from "react";
import "./styles.css";
export default function App() {
const STARTING_TIME = 5;
const [text, updateText] = useState("");
const [time, setTime] = useState(STARTING_TIME);
const [isTimeRunning, setIsTimeRunning] = useState(false);
const [wordCount, setWordCount] = useState(0);
const inputRef = useRef(null)
function handleChange(event) {
const { value } = event.target;
updateText(value);
}
function startGame() {
setIsTimeRunning(true);
setTime(STARTING_TIME);
updateText("");
setWordCount(0);
inputRef.current.disabled = false;
inputRef.current.focus();
}
function endGame() {
setIsTimeRunning(false);
const numWords = countWords(text);
setWordCount(numWords);
}
function countWords(text) {
const wordsArr = text.trim().split(" ");
const filteredWords = wordsArr.filter(word => word !== "");
return filteredWords.length;
}
useEffect(() => {
if (isTimeRunning && time > 0) {
setTimeout(() => {
setTime(time => time - 1);
}, 1000);
} else if (time === 0) {
endGame();
}
}, [time, isTimeRunning]);
return (
<div>
<h1>Speed Typing Game</h1>
<textarea
onChange={handleChange}
value={text}
disabled={!isTimeRunning}
/>
<h4>Time remaning: {time} </h4>
<button type="button" onClick={startGame} disabled={isTimeRunning}>
Start Game
</button>
<h1>Word Count: {wordCount} </h1>
</div>
);
}
Errors shown in the IDE console log below:
You have to attach the inputRef to your textArea.
<textarea
onChange={handleChange}
value={text}
disabled={!isTimeRunning}
ref={inputRef}
/>
Find out more about React Refs here