I have a dynamic Countdown Timer In React:
This Timer is a component (MovementTimer.js) & the main component (GameRoom.js) is calling Timer's render method, passing thru props.
This is the render Timer function that's being called below upon every GameRoom render
renderTimer = () => {
console.log('*** Timer RERENDER ***');
console.log(this.state.turn_time);
console.log(this.state.last_time);
if(this.state.turn_time != null && this.state.last_time != null){
const props = {
time : this.state.turn_time,
last_time : this.state.last_time
}
return(
<MovementTimer {...props}/>
)
} else {
return(
<p>Undefined Timer.</p>
)
}
}
This is the render method of GameRoom.js
render() {
if(this.state.guest){
console.log('loaded in ! ' + this.state.guest);
// Put into return statement for DEBUG {this.state.turn ? "TRUE" : "FALSE"}
return (
<div className="main">
{this.renderBoard()}
{this.renderTimer()}
{}
<p className="welcome">
{this.state.userResponseData}
{
this.state.guest[0] ? (this.state.turn ? "It's your opponent's turn." : "It's your turn, move!") : (this.state.turn ? "It's your turn, move!" : "It's your opponent's turn.")
}</p>
</div>
)
}
return (
<div className="main">
{this.renderBoard()}
<p className="welcome"> {this.state.turn ? "TRUE" : "FALSE"} Loading {this.state.userResponseData}</p>
</div>
)
}
}
Basically the issue is, whenever GameRoom.js rerender's, the renderTimer() function passes in updated props to the MovementTimer, however the timer doesn't reset. I was trying to use a useRef state boolean but it was buggy and wasn't working.
MovementTimer.js component
import React, {useRef, clearState,useState,useCallback} from 'react'
const MovementTimer = (props) => {
console.log('Re initizalized')
const time = parseInt(props.time);
const last_time = parseInt(props.last_time);
//clearState()
console.log(
"Estimated Time:\n"+
((last_time+time*1000) - Date.now() )/1000
);
let [counter, setCounter] = React.useState(Math.round( ((last_time+time*1000) - Date.now() )/1000 ) );
// const reinit = () => {
// setCounter(time)
// }
console.log(counter);
console.log(last_time);
React.useEffect(() => {
counter > 0 && setTimeout(() => setCounter(
counter - 1
), 1000); //Set this too Due time - last time / 1000 (for secs)
if(counter === 0){
const socketProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const socket = new WebSocket(socketProtocol+(window.location.href).split(':')[1]+':5000');
socket.onopen = async () => {
await socket.send(JSON.stringify({
query_type : 't0',
room_id: (window.location.pathname).slice(6)
}));
socket.close();
}
setCounter(Math.round( ((last_time+time*1000) - Date.now() )/1000 ))
}
}, [counter]);
return (
<div>
{counter}
</div>
)
}
export default MovementTimer
In this component, I useEffect for the timer countdown, and pass in the time difference in seconds (using props that have been passed down) Is there anyway to check if props change, and reset the useEffect?
In that if statement (counter === 0), I manually set it back using setCounter. however i dont know how to check for prop change. whenever last_time prop is updated.
The second parameter in your useEffect is what the function listens to for changes. If you just want to rerun the function in useEffect when a specific variable changes, add it to the array. For example, if you want to rerun it when variable x changes, you would write it like this:
React.useEffect(() => { /* do stuff */ }, [counter, x]);
Documentation: Conditionally firing an effect
Related
I have a simple react app where a user inputs a quantity and an api call is made to convert that value into an equivalent amount of a very unstable cryptocurrency, and that converted value is displayed on the screen.
Crucially, I want the converted value to be updated every ten seconds or when a user changes the input value.
I'm confused as to how to implement this. Any advice?
I don't know react, but in native JS it's pretty simple. Here's some code that sets your timer then clears and resets when you call an input change. Hope this is easily adaptable to react!
function cryptoTimer() {
this.value = null
this.timer = null
this.getValue = () => {
this.value = "some api response"
console.log(this.value)
}
this.startTimer = () => {
this.timer = setInterval(() => { this.getValue() }, 10000)
}
this.clearTimer = () => {
clearInterval(this.timer)
}
this.inputChange = (elem) => {
console.log('input changed, value is '+elem.value)
this.clearTimer()
this.init()
}
this.init = () => {
this.getValue()
this.startTimer()
}
this.init()
}
var timer = new cryptoTimer
<!-- some input on change action -->
<input onkeyup="timer.inputChange(this)" type="text" />
Here is an example
import React from "react";
export function App() {
// Used to store the intervalId
const timerRef = React.useRef();
// Used to store the value in the input element
const inputRef = React.useRef("");
// This is for storing the transformed value you get from API
const [transformedValue, setTransformedValue] = React.useState("");
// In this function, you can make the request to the API and update
// the transformed value. Remember to reset the interval.
const transformValue = async () => {
const input = inputRef.current;
// Invoke your api instead
const randomTransformation = `${input} ${new Date().toLocaleTimeString()}`;
setTransformedValue(randomTransformation);
resetInterval();
};
// This will just reset the interval
const resetInterval = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
timerRef.current = setInterval(transformValue, 10 * 1000);
};
return (
<div className="container">
<label htmlFor="input">Input</label>
<input
id="input"
type="text"
value={inputRef.current}
onChange={(e) => {
inputRef.current = e.target.value;
transformValue();
}}
/>
<p>Transformed Value: {transformedValue}</p>
</div>
);
}
I'd like some help understanding why my React Component isn't working. I'm trying work on my React/Js fundamentals by making a basic timer. The component is rendering great, just nothing is happening. I'm looking at the React Profiler on Chrome and my variables seem to be updating correctly.
I know it's basic, I'd just rather start and fix my mistakes and learn on the way.
Any help whatsoever would be greatly appreciated!
import React from 'react'
var sec = 0;
var min = 0;
var hrs = 0;
var timer_id;
// Helper Functions
function tick(){
sec++;
if (sec >= 60){
sec = 0;
min ++;
if (min >= 60){
min = 0;
hrs++;
};
};
}
function add(){
tick();
timer();
}
function timer(){
console.log("TIMER");
timer_id = setTimeout(add, 1000);
}
// Clock Component
class Clock extends React.Component{
render(){
timer();
return (
<div className="clock-face">
<h4> {hrs} : {min} : {sec}</h4>
<button
onClick = {() => clearTimeout(timer_id)}> Stop </button>
</div>
);
}
}
export default Clock;
React components only get rerendered when their props or state changes (or when forced to rerender, but we don't do that). You're updating hrs/min/sec in global variables; those aren't tracked by React.
In addition, it's not a good idea to have render() execute a side effect (timer() in your case); React may elect to render your function more than once.
Here's an example of your clock as a class component that ticks along and cleans the timer up after it's unmounted.
The magic that causes the rerendering here is this.setState being called.
import React from "react";
function divmod(a, b) {
return [Math.floor(a / b), a % b];
}
class Clock extends React.Component {
state = { time: 0 };
componentDidMount() {
this.timer = setInterval(this.tick, 1000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
tick = () => {
this.setState(({ time }) => ({ time: time + 1 }));
};
render() {
const { time } = this.state;
const [hmins, secs] = divmod(time, 60);
const [hrs, mins] = divmod(hmins, 60);
return (
<div className="clock-face">
<h4>
{" "}
{hrs} : {mins} : {secs}
</h4>
</div>
);
}
}
I have a working animation of an object made with the "useRef" hook. Part of the code in this animation will be repeated several times, so I moved it into a separate function, but when I try to call this function, when rendering the component, I get the error "Can't assign to property" scrollLeft "on 1: not an object" what could be the problem?
Full code on codesandbox
https://codesandbox.io/s/peaceful-silence-bm6hx?file=/src/scroll.js
import React, {useState, useEffect, useRef} from 'react'
const Scrollable = props => {
const items = props.items;
let ref = useRef()
const [state, setState] = useState({
isScrolling:false,
clientX:0,
scrollX:0
})
const [touchStart, setTouchStart] = useState(0);
let frameId;
const onMouseDown = e =>{...}
const onMouseUp = e =>{
if(ref && ref.current && !ref.current.contains(e.target)) {
return;
}
e.preventDefault()
let touchShift = touchStart - state.clientX
let rez;
let shift;
if(touchShift > 0) {
shift = 300 - touchShift
rez = state.scrollX + shift
if(rez>2100){
rez =1800
cancelAnimationFrame(frameId)
}
let speed = shift / 20
let cur = state.scrollX
frameId = requestAnimationFrame(animate)
animate(cur,speed,rez)
}
}
const animate = (cur, speed,rez) => {
frameId = requestAnimationFrame(animate)
cur = cur + speed
ref.current.scrollLeft = cur.toFixed(2)
if (Math.round(cur) === rez) {
cancelAnimationFrame(frameId)
setState({
...state,
scrollX:rez,
isScrolling:false,
})
}
}
useEffect(() =>{
document.addEventListener('mousedown',onMouseDown)
document.addEventListener('mouseup',onMouseUp)
return () => {
document.removeEventListener('mousedown',onMouseDown)
document.removeEventListener('mouseup',onMouseUp)
}
})
useEffect(() =>{
ref.current = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(ref.current)
},[]})
return (
<div className={classes.charPage}>
<div
ref={ref}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}>
</div>
</div>
)
}
export default Scrollable;
This error means you're trying to set a property on a number. In your useEffect you're doing this:
ref.current = requestAnimationFrame(animate)
requestAnimationFrame returns, according to MDN:
A long integer value, the request id, that uniquely identifies the entry in the callback list. This is a non-zero value, but you may not make any other assumptions about its value.
But you're also using the same ref for your DOM element. After your useEffect runs it will have set your ref to the rAF id which is a number causing your error when you try to set the scrollLeft property on the ref.
What you can try next to solve this is to use 2 separate refs, one for the requestAnimationFrame and one for your DOM element.
I'm new to learning React and Gatsby, and am trying to find the best way to apply simple a Javascript animation to a DOM element within a component. I know how to handle component events with onClick etc, but say for example I want to continuously change the colour of a <span> in my Header.js component every 2 seconds.
import React from 'react';
export default function Header() {
return (
<header>
<p>This is a <span>test!</span></p>
</header>
)
}
I'd then want to use some JS like:
const spanEl = document.querySelector('header span');
let counter = 0;
const changeColor = () => {
if (counter % 2 == 0) {
spanEl.style.color = "red";
} else {
spanEl.style.color = "blue";
}
counter++;
if (counter == 10) counter = 0;
}
setInterval(changeColor, 2000);
I found that I could put this inside a script tag in html.js before the closing body tag, but is there a way to keep this functionality within the component? Do I need to completely rethink my approach when working within this framework?
If you want to approach this with idiomatic React, then I would recommend expressing this behavior using hooks, component lifecycles, and effects.
The official React docs for hooks and effects are very good, I would start there.
import React from 'react';
const noop = () => null;
// Encapsulate the interval behavior
const useInterval = (callback, delay) => {
const savedCallback = useRef(noop);
useEffect(() => {
savedCallback.current = callback;
savedCallback.current();
}, [callback]);
useEffect(() => {
const id = setInterval(savedCallback.current, delay);
return () => clearInterval(id);
}, [delay]);
};
export default function Header() {
const [color, setColor] = useState("blue");
// setColor causes a re-render of the component
const updateColor = setColor(color === "blue" ? "red" : "blue");
useInterval(updateColor, 2000);
// Use the jsx to change the color instead of reaching into the dom
return (
<header>
<p>This is a <span style={{ color }}>test!</span></p>
</header>
)
}
[EDIT: I've just seen the answer from #windowsill, which I think is better than mine; I would recommend going with that solution.]
In a React functional component, you need to use the useReference hook to target an element (rather than selecting it with document.querySelector()) and the useEffecet hook to set and clear the timeout when the component mounts/unmounts:
import React, {
useEffect,
useRef,
useCallback
} from 'react';
export function Header() {
const animatedText = useRef(null);
const runAnimation = useCallback(elem => {
const currColour = elem.style.color;
elem.style.color = (currColour === 'red' && 'blue') || 'red';
}, []);
useEffect(() => {
const animationInterval = setInterval(() => {
runAnimation(animatedText.current);
}, 2000);
return () => {
clearInterval(animationInterval);
}
}, [runAnimation]);
return (
<header>
<p>This is a <span ref={animatedText}>test!</span></p>
</header>
);
}
The useCallback hook is used for optimization purposes and prevent the function runAnimation from being re-defined and initialized every time the component re-renders.
i have component with text. It changed component own state with mouse click. But i want to save possibility to select and copy in by long click. Is there way to make it ? Selection is reset after rerender component. Code for example:
const App = () => {
const [someState, setSomeState] = React.useState(0);
const clickHandler = () => {
setSomeState(someState + 1);
}
return (
<div
className="App"
onClick={clickHandler}
>
{'State ' + someState}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
How about using onMouseDown and onMouseUp events yourself and calculate the time the user took to click instead of using onClick?
You could for example do something like this:
const App = () => {
const [someState, setSomeState] = React.useState(0);
const [timeDown, setTimeDown] = React.useState(-1);
const clickHandler = () => setSomeState(someState + 1);
const handleMouseDown = () => setTimeDown(Date.now()); // Save the time of the mousedown event
const handleMouseUp = () => {
const timeUp = Date.now();
const timeDiff = timeUp - timeDown; // Calculate the time the user took to click and hold
if (timeDiff < 1000) { // If it's shorter than 1000ms (1s) execute the normal click handler
clickHandler();
} else { // Execute some other logic, or just ignore the click
// handleLongClick();
}
};
return (
<div
className="App"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
{"State " + someState}
</div>
);
};
You can find a quick codesandbox as a demo here