Using ref react hooks multiple times - javascript

I'm trying to use ref API many times with setInterval method but only one is working
I have created a ref as an array then I'm trying to insert the function to the array by using the index key but that works only for the first insert I don't know what I'm doing wrong
here's what I've achieved
import React, { useState, useEffect, useRef, createRef } from "react";
import ReactDOM from "react-dom";
function Counter() {
const [countSec, setCountSec] = useState(0);
const [countMin, setCountMin] = useState(0);
useInterval(() => {
setCountSec(countSec + 1); // this working
}, 1000, 0);
useInterval(() => {
setCountMin(countMin + 1); // it's not working
}, 1100, 1);
return <div>
<h1>{countSec} Secounds</h1>
<h1>{countMin} Half-Minutes</h1>
</div>;
}
function useInterval(callback, delay,index){
const savedCallback = useRef([...Array(2)].map(()=> createRef()));
// Remember the latest function.
useEffect(() => {
savedCallback.current[index].current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
let id = setInterval(()=>savedCallback.current[index].current(), delay);
return () => clearInterval(id);
});
};
https://codesandbox.io/s/sharp-tree-k39ts

There is no need to try to remember multiple callbacks in your useRef. Everytime you use useInterval it creates a new instance of your hook. So they will work independently from eachother. This means the first time you use useInterval it will create a new useRef for your setCountSec(countSec + 1) callback. And the second time you use useInterval it will create another instance of useRef for your setCountMin(countMin + 1). All you have to do it change the interval to have the second one update once every 30 seconds.
import React, { useState, useEffect, useRef, createRef } from "react";
import ReactDOM from "react-dom";
function Counter() {
const [countSec, setCountSec] = useState(0);
const [countMin, setCountMin] = useState(0);
useInterval(() => {
setCountSec(countSec + 1);
}, 1000); // update every 1000ms (1sec)
useInterval(() => {
setCountMin(countMin + 1);
}, 30000); // update every 30000ms (30sec)
return <div>
<h1>{countSec} Secounds</h1>
<h1>{countMin} Half-Minutes</h1>
</div>;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest function.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

Based on your comment
you need to update countMin after countSec
Here is an working code
function Counter() {
const [countSec, setCountSec] = useState(0);
const [countMin, setCountMin] = useState(0);
useEffect(()=>{
setInterval(() => {
setCountSec(countSec + 1); // this working
}, 1000);
setInterval(() => {
setCountMin(countMin + 1); // it's not working
}, 1100);
}, [])
return <div>
<h1>{countSec} Secounds</h1>
<h1>{countMin} Half-Minutes</h1>
</div>;
}
few pointers on your code would be for every rerender the whole function would get called and a new setInterval would be created...
useEffect(()=>{},[]) is equivalent to componentDidMount
if you want to follow your patten without this useEffect(()=>{},[]) you could use setTimeout

Related

functional component still rerender with react.memo

I want to make a quiz app with a countdown timer.
The timer use setinterval and the quiz has a functional component for Question.
I have already set react.memo for the question function component. But it keeps rerender each second. It works fine on desktop. But it flickers on mobile since the input for answer keep refocus
import { useEffect, useState, memo } from 'react';
function Game() {
let interval;
const [timer, setTimer] = useState(180000);
useEffect(() => {
interval = setInterval(() => {
setTimer(timer - 1000);
}, 1000);
return () => {
if (interval) {
clearInterval(interval)
}
}
}, [timer]);
const QuestionComponent = memo(props => {
return(<div></div>);
});
return (
<>
<div>{timer}</div>
<QuestionComponent/>
</>
);
}
The above code is the Game component, which will be called from App component
The question component keeps rerendering even there is nothing in it.
May anyone please advise? Thank you very much
Ideally you should define your components outside your other components. As mentioned in comment, this line of code will run on every render.
Even if you use React.memo(), this line itself will run on every render, in contrast to writing it outside where it will run normally.
If you are writing it inside, you have to persist the value between renders. It can be done using useCallback because a react component is technically a function:
const Component = useCallback(
memo(() => {
console.log("rerender");
return <div></div>;
}),
[]
);
Or another way is using a ref.
import { useEffect, useState, memo, useMemo, useCallback, useRef } from "react";
function App() {
let interval;
const [timer, setTimer] = useState(180000);
const componentRef = useRef();
useEffect(() => {
interval = setInterval(() => {
setTimer(timer - 1000);
}, 1000);
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [timer]);
useEffect(() => {
const Component = memo(() => {
console.log("rerender");
return <div></div>;
});
componentRef.current = Component;
}, []);
return (
<>
<div>{timer}</div>
<>{componentRef.current && <componentRef.current />}</>
</>
);
}
export default App;
Link
But both above are hacks. And you will definitely run into issues when you use hooks.
Much better to just write it outside.
const QuestionComponent = memo(props => {
return(<div></div>);
});
import { useEffect, useState, memo } from 'react';
function Game() {
let interval;
const [timer, setTimer] = useState(180000);
useEffect(() => {
interval = setInterval(() => {
setTimer(timer - 1000);
}, 1000);
return () => {
if (interval) {
clearInterval(interval)
}
}
}, [timer]);
return (
<>
<div>{timer}</div>
<QuestionComponent/>
</>
);
}

How to call a function every minute in a React component?

I make a table to get stock price quotes, it works well, but when I try to put a function include setState in the component, it falls into an infinite loop, it triggers setState and re-render immediately and triggers again.
How can I call this function without triggering an infinite loop when I load this component?
I would to call the function every 10 seconds or every minute.
import React, { useState } from 'react'
import api from '../../api'
function CreateRow(props){
const [stock, setStock] = useState({symbol:'',last:'',change:''})
async function check() {
const result = await api.getStock(props.item)
console.log(props.item)
const symbol = result.data.symbol
const lastest = result.data.latestPrice
const change = result.data.change
setStock({symbol:symbol, lastest:lastest, change:change})
}
// check() <----------! if I call the function here, it becomes an infinite loop.
return(
<tr>
<th scope="row"></th>
<td>{stock.symbol}</td>
<td>{stock.lastest}</td>
<td>{stock.change}</td>
</tr>
)
}
export default CreateRow
You want to initiate a timeout function inside a lifecycle method.
Lifecycle methods are methods which call on, for example, mount and unmount (there are more examples but for the sake of explanation I will stop here)
what you're interested in is the mount lifecycle.
In functional components, it can be accessed like this:
import { useEffect } from 'react';
useEffect(() => {
// This will fire only on mount.
}, [])
In that function, you want to initialize a setTimeout function.
const MINUTE_MS = 60000;
useEffect(() => {
const interval = setInterval(() => {
console.log('Logs every minute');
}, MINUTE_MS);
return () => clearInterval(interval); // This represents the unmount function, in which you need to clear your interval to prevent memory leaks.
}, [])
Consider 60000 milliseconds = 1 minute
Can do using the method:
setInterval(FunctionName, 60000)
do as below:
async function check() {
const result = await api.getStock(props.item)
console.log(props.item)
const symbol = result.data.symbol
const lastest = result.data.latestPrice
const change = result.data.change
setStock({symbol:symbol, lastest:lastest, change:change})
}
// Write this line
useEffect(() => {
check()
}, []);
setInterval(check, 60000);
you also do that with setTimeout
import React, { useState, useEffect } from "react";
export const Count = () => {
const [counts, setcounts] = useState(0);
async function check() {
setcounts(counts + 1);
}
// Write this line
useEffect(() => {
check();
}, []);
console.log("hello dk - ",counts)
setTimeout(() => {
check();
}, 1000);
return <div>Count : - {counts}</div>;
};
import React, { useState, useEffect } from "react";
export const Count = () => {
const [currentCount, setCount] = useState(1);
useEffect(() => {
if (currentCount <= 0) {
return;
}
const id = setInterval(timer, 1000);
return () => clearInterval(id);
}, [currentCount]);
const timer = () => setCount(currentCount + 1);
console.log(currentCount);
return <div>Count : - {currentCount}</div>;
};

Why does my timer component behave strangely when passing a dependency to useEffect?

I have the following timer using useEffect and pass a function dependency to it:
const Timer = () => {
const [count, setCount] = useState(0);
const setId = () => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}
useEffect(() => {
setId();
}, [setId])
}
However the timer behaves strangely: the first several seconds is normal, then it starts showing the count randomly. What caused the problem? What's the correct way of doing it?
Check this snippet, A good doc by Dan
import React, { useCallback, useState, useEffect, useRef } from "react";
import "./styles.css";
export default function App() {
let [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <div className="App">{count}</div>;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Working codesandbox
Update
As keith suggested don't pass the function to array deps as shown follow.
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <div>{count}</div>;
}
clear the interval on unmount
useEffect(() => {
let id;
const setId = () => {
id = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
};
setId();
return () => clearInterval(id);
}, []);
#DILEEP THOMAS update answer for something like this is how I would do it.
But #thinkvantagedu comment
still not too sure why it is okay to pass []
Maybe needs more explaining, unfortunately too much for comments so I'm posting this.
To answer this, we need to take a step back, and first ask, what is the point of the dependency array?. The simple answer is anything inside the useEffect that depends on something that if changed from say either useState or Props that would require different handling, it would want putting in the array.
For example, if say you had a component that simply displayed a userProfile, but getting this info was async, so requires a useEffect. It might look something like this ->
<UserProfile userId={userId}/>
Now the problem here is if we passed [] as the dependency, the useEffect would not re-fire for the the new userId, so the props might say userId = 2, but the data we currently have stored in state is for userId = 1,.. So for something like this [props.userId] would make total sense.
So back to the OP's component <App/> what is there here that would change?, well it's not the props, as none are passed. So what about count you might ask, well again ask yourself the question does the state of count warrant a new instance of a setInterval been destroyed / created?. And of course the answer here is no, and as such passing [] makes total sense here.
Hope that make sense,.

React interval using old state inside of useEffect

I ran into a situation where I set an interval timer from inside useEffect. I can access component variables and state inside the useEffect, and the interval timer runs as expected. However, the timer callback doesn't have access to the component variables / state. Normally, I would expect this to be an issue with "this". However, I do not believe "this" is the the case here. No puns were intended. I have included a simple example below:
import React, { useEffect, useState } from 'react';
const App = () => {
const [count, setCount] = useState(0);
const [intervalSet, setIntervalSet] = useState(false);
useEffect(() => {
if (!intervalSet) {
setInterval(() => {
console.log(`count=${count}`);
setCount(count + 1);
}, 1000);
setIntervalSet(true);
}
}, [count, intervalSet]);
return <div></div>;
};
export default App;
The console outputs only count=0 each second. I know that there's a way to pass a function to the setCount which updates current state and that works in this trivial example. However, that was not the point I was trying to make. The real code is much more complex than what I showed here. My real code looks at current state objects that are being managed by async thunk actions. Also, I am aware that I didn't include the cleanup function for when the component dismounts. I didn't need that for this simple example.
The first time you run the useEffect the intervalSet variable is set to true and your interval function is created using the current value (0).
On subsequent runs of the useEffect it does not recreate the interval due to the intervalSet check and continues to run the existing interval where count is the original value (0).
You are making this more complicated than it needs to be.
The useState set function can take a function which is passed the current value of the state and returns the new value, i.e. setCount(currentValue => newValue);
An interval should always be cleared when the component is unmounted otherwise you will get issues when it attempts to set the state and the state no longer exists.
import React, { useEffect, useState } from 'react';
const App = () => {
// State to hold count.
const [count, setCount] = useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
export default App;
You can run the code and see this working below.
const App = () => {
// State to hold count.
const [count, setCount] = React.useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
React.useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
ReactDOM.render(<App />, document.getElementById('app'))
<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>
<div id="app"></div>
If you need a more complex implementation as mention in your comment on another answer, you should try using a ref perhaps. For example, this is a custom interval hook I use in my projects. You can see there is an effect that updates callback if it changes.
This ensures you always have the most recent state values and you don't need to use the custom updater function syntax like setCount(count => count + 1).
const useInterval = (callback, delay) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay])
}
// Usage
const App = () => {
useInterval(() => {
// do something every second
}, 1000)
return (...)
}
This is a very flexible option you could use. However, this hook assumes you want to start your interval when the component mounts. Your code example leads me to believe you want this to start based on the state change of the intervalSet boolean. You could update the custom interval hook, or implement this in your component.
It would look like this in your example:
const useInterval = (callback, delay, initialStart = true) => {
const [start, setStart] = React.useState(initialStart)
const savedCallback = React.useRef()
React.useEffect(() => {
savedCallback.current = callback
}, [callback])
React.useEffect(() => {
if (start && delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay, start])
// this function ensures our state is read-only
const startInterval = () => {
setStart(true)
}
return [start, startInterval]
}
const App = () => {
const [countOne, setCountOne] = React.useState(0);
const [countTwo, setCountTwo] = React.useState(0);
const incrementCountOne = () => {
setCountOne(countOne + 1)
}
const incrementCountTwo = () => {
setCountTwo(countTwo + 1)
}
// Starts on component mount by default
useInterval(incrementCountOne, 1000)
// Starts when you call `startIntervalTwo(true)`
const [intervalTwoStarted, startIntervalTwo] = useInterval(incrementCountTwo, 1000, false)
return (
<div>
<p>started: {countOne}</p>
<p>{intervalTwoStarted ? 'started' : <button onClick={startIntervalTwo}>start</button>}: {countTwo}</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('app'))
<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>
<div id="app"></div>
The problem is the interval is created only once and keeps pointing to the same state value. What I would suggest - move firing the interval to separate useEffect, so it starts when the component mounts. Store interval in a variable so you are able to restart it or clear. Lastly - clear it with every unmount.
const App = () => {
const [count, setCount] = React.useState(0);
const [intervalSet, setIntervalSet] = React.useState(false);
React.useEffect(() => {
setIntervalSet(true);
}, []);
React.useEffect(() => {
const interval = intervalSet ? setInterval(() => {
setCount((c) => {
console.log(c);
return c + 1;
});
}, 1000) : null;
return () => clearInterval(interval);
}, [intervalSet]);
return null;
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

Unable to access state inside interval

I am unable to access the state within an interval. Here I want to access counter inside interval when the counter gets equal to 10 I want to stop it.
Note: Here I don't want to put an interval inside useEffect because I need to start the interval in a specific time, by handling an event.
export default props => {
const [counter, setCounter] = useState(0);
const startInterval = () => {
const timeout = setInterval(() => {
setCounter(counter + 1);
console.log("counter: ", counter); // alway return 0
if(counter === 10) clearInterval(timeout);
}, 1000);
};
}
As I am seeing here even your setCounter(counter+1) wont update, because of lexical scope. So you have to change it like this:
setCounter(counter => counter + 1);
Also because of lexical scope you wont access counter to check condition, so you have to make a variable and update that inside functional component by asigning counter to it, then check it with if condition.
Complete Code code:
let myCounter = 0;
let timeout = null;
export default CounterApp = props => {
const [counter, setCounter] = useState(0);
// Also don't forget this
useEffect(()=> {
return ()=> clearInterval(timeout);
}, []);
myCounter = counter;
const startInterval = () => {
timeout = setInterval(() => {
setCounter(counter => counter + 1);
console.log("counter: ", myCounter); // counter always return 0 but myCounter the updated value
if(myCounter === 10) clearInterval(timeout);
}, 1000);
};
}
I came across this exact problem not too long ago. Hooks don't work exactly as you'd expect in relation to setInterval. I found the solution on Dan Abramov's blog: You can useRef to combine multiple useEffects. Through his useInterval implementation you can also start and stop the interval by setting the timer to null
import React, { useState, useEffect, useRef } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [time, setTime] = useState(null); // timer doesn't run initially
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, time);
return <>
<h1>{count}</h1>
<div onClick={() => setTime(1000)}>Start</div>
<div onClick={() => setTime(null)}>Stop</div>
</>;
}
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

Categories

Resources