According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect() hook to simulate componentDidUpdate(), but it seems like useEffect() is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction is printed during the initial render but componentDidUpdateClass was not printed during the initial render.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.log("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
We can use the useRef hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect function is being run.
If we want the effect to run in the same phase that componentDidUpdate does, we can use useLayoutEffect instead.
Example
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
I made a simple useFirstRender hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true, then switches to false in the useEffect, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ....
Same approach as Tholle's answer, but using useState instead of useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect without a dependency array (useEffect(() => {...}) will be run again.
Using and updating useRef will not cause any re-renders.
#ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* #param {function:function} effect - A useEffect effect.
* #param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};
a simple way is to create a let, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to #Carmine Tambasciabs solution but without using state :)
function useEffectAfterMount(effect, deps) {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) return effect();
else isMounted.current = true;
}, deps);
// reset on unmount; in React 18, components can mount again
useEffect(() => {
isMounted.current = false;
});
}
We need to return what comes back from effect(), because it might be a cleanup function. But we don't need to determine if it is or not. Just pass it on and let useEffect figure it out.
In an earlier version of this post I said resetting the ref (isMounted.current = false) wasn't necessary. But in React 18 it is, because components can remount with their previous state (thanks #Whatabrain).
I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem that triggers the useEffect callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}
This is the best implementation I've created so far using typescript. Basically, the idea is the same, using the Ref but I'm also considering the callback returned by useEffect to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* #param effect
* #param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect hook but this time, it won't run on the initial render. Here is how you can use this hook.
useNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}
#MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current value to false. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
For people who are having trouble with React 18 strict mode calling the useeffect on the initial render twice, try this:
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);
All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing"
Have a component rendered with different information from those API.
The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done
Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App
const [dojob, setDojob] = useState(false);
yourfunction(){
setDojob(true);
}
useEffect(()=>{
if(dojob){
yourfunction();
setDojob(false);
}
},[dojob]);
import React, { useState } from "react";
import useInterval from "use-interval";
const useOwnHook = () => {
const arr = [...Array(100)].map((_, index) => index);
return {
arr
};
};
const Component = ({ count }) => {
const { arr } = useOwnHook();
console.log(arr, "arr");
return (
<div className="App">
<h1>{count + 1}</h1>
</div>
);
};
export default function App() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <Component count={count} />;
}
I've created hook useOwnHook for demonstration that each time Component re-render, each time it goes insite useOwnHook, and create new array, is it possible to prevent it to move inside of this hook each time on re-render?
Personally, I'd use a state variable with a init function:
const useOwnHook = () => {
const [arr] = useState(() => [...Array(100)].map((_, index) => index));
return {
arr
};
};
Benefit of the init function is that it's lazy-evaluated so you won't be constructing the array each time the component is rendered.
You can add useState into your custom hook as:
const useOwnHook = () => {
const [arr] = useState([...Array(100)].map((_, index) => index));
return {
arr
};
};
By doing this you can keep the same array in your useOwnHook.
Also you can import as import { useState } from 'react'.
See also from the Using the State Hook documentation - example with a different variable:
We declare a state variable called count, and set it to 0. React will remember its current value between re-renders, and provide the most recent one to our function. If we want to update the current count, we can call setCount.
I am currently developing a simple weather app using openweatherapp API. The app is developed to fetch data from two endpoints: one that returns the current weather in your city and the other one that returns the weather forecast for next 5 days. The app should also fire an event after 60 seconds that re-fetches the data. This is how I tried to architecture my solution:
In App.js I am fetching the data and then I am passing it down as props to two other components, one that handles the current weather and the other one, the weather forecast. In the CurrentWeatherForecast component I am also initiating the function that updates the state every second using hooks. When the timer reaches 60 seconds I am calling the "handleRefresh" function that I have passed down as a prop from App.js. (in App.js is where the actual update happens). The "handleRefresh" function is outside the render method of App.js and it updates a "step" variable that should then cause the component to re-render and to re-fetch the data. The issue is that upon calling setState the function causes an infinite loop which I don't understand why since the function is outside the render method. I will post my code below.
import React, { Component } from "react";
import { CurrentWeatherForecast } from "./components/CurrentWeatherForecast";
import { NextDaysWeatherForecast } from "./components/NextDaysWeatherForecast";
export class App extends Component {
constructor(props) {
super(props);
this.state = {
currentWeather: [],
nextDaysWeather: [],
step: 0,
};
}
componentDidMount() {
const { step } = this.state;
var currentWeather;
var nextDaysWeather; // step is used to indicate wether I want to fetch data or not
if (step === 0) {
fetch(
"https://api.openweathermap.org/data/2.5/weather?q=London&appid=1fc71092a81b329e8ce0e1ae88ef0fb7"
)
.then((response) => {
const contentType = response.headers.get("content-type");
if (
!contentType ||
!contentType.includes("application/json")
) {
throw new TypeError("No JSON data!");
}
return response.json();
})
.then((data) => {
currentWeather = data;
})
.catch((error) => console.error(error));
fetch(
"https://api.openweathermap.org/data/2.5/forecast?q=London&appid=1fc71092a81b329e8ce0e1ae88ef0fb7"
)
.then((response) => {
const contentType = response.headers.get("content-type");
if (
!contentType ||
!contentType.includes("application/json")
) {
throw new TypeError("No JSON data!");
}
return response.json();
})
.then((data) => {
let requiredData = data.list.slice(0, 5);
nextDaysWeather = requiredData;
})
.catch((error) => console.error(error));
let f = setTimeout(() => {
this.setState({
currentWeather: currentWeather,
nextDaysWeather: nextDaysWeather,
step: 1, // updating step to 1 after fetching the data
});
}, 1000);
}
}
handleRefresh = () => {
const { step } = this.state;
console.log(step);
this.setState({ step: 0 }); // updating the step to 0 this causes the infinite loop
};
render() {
const { currentWeather, nextDaysWeather } = this.state;
return (
<div>
<CurrentWeatherForecast
currentWeather={currentWeather}
handleRefresh={this.handleRefresh}
/>
<NextDaysWeatherForecast nextDaysWeather={nextDaysWeather} />
</div>
);
}
}
export default App;
This was in App.js Ignore the NextDaysWeatherForecast component as it is empty for now
import React, { useEffect, useState } from "react";
export const CurrentWeatherForecast = (props) => {
const { currentWeather } = props;
const [progressValue, setValue] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setValue((progressValue) =>
progressValue < 61 ? progressValue + 1 : (progressValue = 0)
);
}, 1000);
return () => clearInterval(interval);
}, []);
if (progressValue === 60) {
props.handleRefresh(); // calling the handleRefresh function passed from App.js
}
return (
<div>
<label htmlFor="file">Downloading progress:</label>
<progress id="file" value={progressValue} max="60">
{progressValue}%
</progress>
</div>
);
};
And this was the NextWeatherForecast component where I am initiating the timer and then calling the "handleRefresh" function that I have passed down as a prop.
Thanks in advance guys !
Have a look at this effect-phase and render-phase code, and try to guess what's wrong.
useEffect(() => {
const interval = setInterval(() => {
setValue((progressValue) =>
progressValue < 61 ? progressValue + 1 : (progressValue = 0)
);
}, 1000);
return () => clearInterval(interval);
}, []);
if (progressValue === 60) {
props.handleRefresh(); // calling the handleRefresh function passed from App.js
}
This one in particular smells like an overflow: a rerender-causing function called during the render phase (and we know handleRefresh to cause rerenders.
if (progressValue === 60) {
props.handleRefresh(); // calling the handleRefresh function passed from App.js
}
Now, let's look for something that is supposed to stop the overflow (that is, it tries to set progressValue to something else than 60, once its 60).
Here it is:
progressValue < 61 ? progressValue + 1 : (progressValue = 0)
Except, this fires only every 1000ms. Which means for a whole second your component is stuck in a rerender-loop. Once it is set to 60, React starts rendering like crazy and in a very short time gets past the render limit, while progressValue is still many, many milliseconds away from being set to 0.
An example solution would be to check for progressValue === 60 in another effect.
export const CurrentWeatherForecast = (props) => {
const { currentWeather } = props;
const [progressValue, setValue] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setValue(prevProgressValue => prevProgressValue === 60 ? 0 : prevProgressValue + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => progressValue === 60 && props.handleRefresh(), [progressValue]);
return (
<div>
<label htmlFor="file">Downloading progress:</label>
<progress id="file" value={progressValue} max="60">
{progressValue}%
</progress>
</div>
);
};
try this:
import React, { useEffect, useState } from "react";
export const CurrentWeatherForecast = ({ currentWeather }) => {
useEffect(() => {
const interval = setInterval(() => {
props.handleRefresh();
}, 60000);
return () => clearInterval(interval);
}, []);
return (
<div>
your codes goes here...
</div>
);
};
I am trying to useFocusEffect to rerender a component in my view when I focus the view.
I did:
const [theKey, setTheKey] = useState(0);
Then:
useFocusEffect(() => { setTheKey(theKey + 1) }, [theKey]);
And the jsx:
<SwipeListView key={theKey} />
It does not work well, I have the errror: Maximum update depth exceeded
Can someone share a way to rerender it?
I do not have this issue with react router.
Issue is here:
useFocusEffect(() => { setTheKey(theKey + 1) }, [theKey]);
Inside this function you update theKey. And each time theKey gets updated the effect gets called again. This results in an infinite loop.
There are 2 solutions:
Remove theKey dependency:
useFocusEffect(
() => { setTheKey(theKey + 1) },
["replace with something else"]
);
Add a condition before updating the state:
useFocusEffect(
() => { if ("some condition") setTheKey(theKey + 1) },
[theKey]
);
This will prevent the infinite loop.
I also encountered issue with useFocusEffect. Either it triggers infinite loop / render, or it keeps a stale version of the function.
const [count, setCount] = useState(1);
const doSomething = useCallback(() => {
console.log(count);
setCount(count + 1);
}, [count]);
useFocusEffect(
useCallback(() => {
doSomething(); // Count will always be 1 (cached value)
}, [doSomething])
);
useFocusEffect(
useCallback(() => {
doSomething(); // Latest count, but infinite loop due to doSomething() is recreated when count changes
}, [doSomething])
);
Instead, can try with the combination of useIsFocus and usePrevious which works well with existing useEffect method.
import { useIsFocused } from "#react-navigation/native";
import { useEffect, useRef } from "react";
// usePrevious custom hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const isFocused = useIsFocused();
const prevIsFocused = usePrevious(isFocused);
useEffect(() => {
if (!prevIsFocused && isFocused) {
// Run your code here
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFocused]);
So the timer works. If I hard code this.state with a specific countdown number, the timer begins counting down once the page loads. I want the clock to start counting down on a button click and have a function which changes the null of the state to a randomly generated number. I am a bit new to React. I am know that useState() only sets the initial value but if I am using a click event, how do I reset useState()? I have been trying to use setCountdown(ranNum) but it crashes my app. I am sure the answer is obvious but I am just not finding it.
If I didnt provide enough code, please let me know. I didn't want to post the whole shebang.
here is my code:
import React, { useState, useEffect } from 'react';
export const Timer = ({ranNum, timerComplete}) => {
const [ countDown, setCountdown ] = useState(ranNum)
useEffect(() => {
setTimeout(() => {
countDown - 1 < 0 ? timerComplete() : setCountdown(countDown - 1)
}, 1000)
}, [countDown, timerComplete])
return ( <p >Countdown: <span>{ countDown }</span> </p> )
}
handleClick(){
let newRanNum = Math.floor(Math.random() * 20);
this.generateStateInputs(newRanNum)
let current = this.state.currentImg;
let next = ++current % images.length;
this.setState({
currentImg: next,
ranNum: newRanNum
})
}
<Timer ranNum={this.state.ranNum} timerComplete={() => this.handleComplete()} />
<Button onClick={this.handleClick} name='Generate Inputs' />
<DisplayCount name='Word Count: ' count={this.state.ranNum} />
You should store countDown in the parent component and pass it to the child component. In the parent component, you should use a variable to trigger when to start Timer.
You can try this:
import React from "react";
export default function Timer() {
const [initialTime, setInitialTime] = React.useState(0);
const [startTimer, setStartTimer] = React.useState(false);
const handleOnClick = () => {
setInitialTime(5);
setStartTimer(true);
};
React.useEffect(() => {
if (initialTime > 0) {
setTimeout(() => {
console.log("startTime, ", initialTime);
setInitialTime(initialTime - 1);
}, 1000);
}
if (initialTime === 0 && startTimer) {
console.log("done");
setStartTimer(false);
}
}, [initialTime, startTimer]);
return (
<div>
<buttononClick={handleOnClick}>
Start
</button>
<Timer initialTime={initialTime} />
</div>
);
}
const Timer = ({ initialTime }) => {
return <div>CountDown: {initialTime}</div>;
};
useState sets the initial value just like you said, but in your case I don't think you want to store the countDown in the Timer. The reason for it is that ranNum is undefined when you start the application, and gets passed down to the Timer as undefined. When Timer mounts, useEffect will be triggered with the value undefined which is something you don't want since it will trigger the setTimeout. I believe that you can store countDown in the parent of the Timer, start the timeout when the button is clicked from the parent and send the countDown value to the Timer as a prop which would make the component way easier to understand.
Here is a simple implementation using hooks and setInterval
import React, {useState, useEffect, useRef} from 'react'
import './styles.css'
const STATUS = {
STARTED: 'Started',
STOPPED: 'Stopped',
}
export default function CountdownApp() {
const [secondsRemaining, setSecondsRemaining] = useState(getRandomNum())
const [status, setStatus] = useState(STATUS.STOPPED)
const handleStart = () => {
setStatus(STATUS.STARTED)
}
const handleStop = () => {
setStatus(STATUS.STOPPED)
}
const handleRandom = () => {
setStatus(STATUS.STOPPED)
setSecondsRemaining(getRandomNum())
}
useInterval(
() => {
if (secondsRemaining > 0) {
setSecondsRemaining(secondsRemaining - 1)
} else {
setStatus(STATUS.STOPPED)
}
},
status === STATUS.STARTED ? 1000 : null,
// passing null stops the interval
)
return (
<div className="App">
<h1>React Countdown Demo</h1>
<button onClick={handleStart} type="button">
Start
</button>
<button onClick={handleStop} type="button">
Stop
</button>
<button onClick={handleRandom} type="button">
Random
</button>
<div style={{padding: 20}}>{secondsRemaining}</div>
<div>Status: {status}</div>
</div>
)
}
function getRandomNum() {
return Math.floor(Math.random() * 20)
}
// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
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])
}
Here is a link to a codesandbox demo: https://codesandbox.io/s/react-countdown-demo-random-c9dm8?file=/src/App.js