useState not firing after page is navigated to - reactjs

The first time the Test component is navigated to, everything works correctly. React Navigation 5 is used to navigate to the Results page, and then navigate back to the Test component. However, although I receive the timerIsRunning prop correctly via the route.params this is not set and passed to the <Timer> as I would expect.
Have I misunderstood how this is meant to work?
Test component (abridged)
export const Test = ({ navigation, route }) => {
const { timed, category, minutes, timerIsRunning } = route.params; // data is received correctly
const [timerRunning, setTimerRunning] = React.useState(timerIsRunning); // this works the first time it's navigated to
return (
<View style={{ flex: 1, margin: 10 }}>
<ScrollView>
{timed ? <Timer seconds={minutes*60} timerRunning={timerRunning} /> : null} // timer component called here
</ScrollView>
<Button
onPress={() => {
setResponseDetails({ answered: false, index: null });
setActiveIndex(0);
setStoredResponses([]);
setTimerRunning(false); // this correctly stops the timer
navigation.navigate('Results', { // and navigates to the Results page
data: storedResponses,
category: category,
timed: timed,
minutes: minutes,
});
}}
disabled={!responseDetails.answered}
style={styles.button}
>
<Text>Results</Text>
<Icon name="arrow-dropright" />
</Button>
)
</View>
);
};
Results page (abridged)
export const Results = ({ navigation, route }) => {
const { category, data: testData, timed, minutes } = route.params;
return (
<View>
<Button
onPress={() =>
navigation.navigate('Test', { // Navigates back to test
timed: timed,
minutes: minutes,
category: category,
timerIsRunning: true // Correctly passes this prop, and is received in the Test component
})
}
>
<Icon name="arrow-back" />
<Text>Try the {category} test again</Text>
</Button>
</View>
);
};

Have you tried adding timerActive as one of your dependencies on the second useEffect?
React.useEffect(() => {
if (timeLimit > 0 && timerActive) {
setTimeout(() => {
// console.log('startTime, ', timeLimit);
settimeLimit(timeLimit - 1);
}, 1000);
}
if (timeLimit === 0 && timerRunning || !timerRunning) {
console.log('done');
}
}, [timeLimit, timerActive]);

Here is an example of a working timer that takes in seconds and timerRunning as props, please let me know if you need any help implementing it. You can update your question or add a working snippet.
const pad = (num) => `0${String(num)}`.slice(-2);
//moved formatTime out of the component
const formatTime = (timeLimit) => {
let displaySeconds = timeLimit % 60;
let minutes = Math.floor(timeLimit / 60);
//clean up formatting number
return `${pad(minutes)}:${pad(displaySeconds)}`;
};
const Timer = ({ seconds, timerRunning }) => {
const [timeLimit, settimeLimit] = React.useState(seconds);
//removed timerActive as it's a copy of timerRunning
React.useEffect(
() => {
if (timerRunning) {
//changed this to a interval and cancel it
// when timeLimit or timerRunning changes
let interval;
if (timerRunning) {
interval = setInterval(() => {
//using callback to get current limit value
// prevent using stale closure or needless
// dependency
settimeLimit((timeLimit) => {
//do the check here so timeLimit is not
// a dependency of this effect
if (timeLimit !== 0) {
return timeLimit - 1;
}
//do not keep running the interval
// if timeLimit is 0
clearInterval(interval);
return timeLimit;
});
}, 1000);
}
//clean up function will end interval
return () => clearInterval(interval);
}
},
//re run if seconds or timerRunning changes
[seconds, timerRunning]
);
//set local timeLimit again when seconds change
React.useEffect(() => settimeLimit(seconds), [seconds]);
return <div> {formatTime(timeLimit)}</div>;
};
function App() {
const [seconds, setSeconds] = React.useState(10);
const [running, setRunning] = React.useState(false);
return (
<div>
<div>
<label>
run timer
<input
type="checkbox"
checked={running}
onChange={() => setRunning((r) => !r)}
/>
</label>
<div>
<label>
<select
onChange={(e) =>
setSeconds(Number(e.target.value))
}
value={seconds}
>
<option value={1}>1</option>
<option value={10}>10</option>
<option value={100}>100</option>
<option value={1000}>1000</option>
</select>
</label>
</div>
</div>
<Timer seconds={seconds} timerRunning={running} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Okay, actually I worked this out - with React Navigation 5.x you can use the route.params in the useEffect to trigger an action. I ended up with this:
React.useEffect(() => {
setTimerRunning(timerIsRunning);
}, [route.params]);

Related

Create the setTimeout in the parent and closes it in the children

I have a specific problem that is keeping me awake this whole week.
I have a parent component which has a pop-up children component. When I open the page the pop-up shows off and after 5 seconds it disappears with a setTimeout.
This pop-up has an input element in it.
I want the pop-up to disappear after 5 seconds or if I click to digit something in the input. I tried to create a timerRef to the setTimeout and closes it in the children but it didn't work.
Can you help me, please? Thanks in advance.
ParentComponent.tsx
const ParentComponent = () => {
const [isVisible, setIsVisible] = useState(true)
timerRef = useRef<ReturnType<typeof setTimeout>>()
timerRef.current = setTimeout(() => {
setIsVisible(false)
}, 5000)
useEffect(() => {
return () => clearTimeout()
})
return (
<div>
<ChildrenComponent isVisible={isVisible} inputRef={timerRef} />
</div>
)
}
ChildrenComponent.tsx
const ChildrenComponent = ({ isVisible, inputRef}) => {
return (
<div className=`${isVisible ? 'display-block' : 'display-none'}`>
<form>
<input onClick={() => clearTimeout(inputRef.current as NodeJS.Timeout)} />
</form>
</div>
)
}
You're setting a new timer every time the the component re-renders, aka when the state changes which happens in the timeout itself.
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
Instead you can put the initialization in a useEffect.
useEffect(() => {
if (timerRef.current) return;
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
return () => clearTimeout(timerRef.current);
}, []);
You should also remove the "loose" useEffect that runs on every render, this one
useEffect(() => {
return () => clearTimeout();
});

RTK Query with Date- Range Picker, avoid unnecessary renders and abide rule of hooks

I have a timecard component that in use could have large amounts of data per user and so it does not make sense to fetch the data until a date-range is supplied. With RTK Query, a hook is used to make the api call (e.x. {data: timecards, isLoading, isSuccess} = useGetTimecardsQuery(userId) ). Using the material ui Date-Range-Picker (or any date-range picker), the values are initially null or "" until the user selects the dates, which then sets the value. In order to delay the fetch, a common approach is to place the fetch into a function or hook to wait until the dates are selected and a button is pressed. However, since RTK Query uses a hook, it then breaks the rule of hooks by placing it not at the top level body of the component's function. I've tried making the search call it's own component and managed to not get any errors, but passing the component to the onClick fails to actually make the call.
Been digging around and not finding any discussion on this, and it could be theres something with React/ RTK that I'm missing. Any help would be greatly appreciated.
Timecard component: (all the commented out lines were different attempts at getting this to work)
export default function Timecard() {
const { user } = useContext(UserContext)
//const [isSearching, setIsSearching] = useState(false)
//const isMounted = useIsMounted();
// const [state, doFetch] = useAsyncFn(async () => {
// const response = await handleSearch();
// return response
// }, [])
const userId = user._id;
//const { data: brackets, isLoading: isBracketsLoading, isSuccess: isBracketsSuccess } = useGetTimeBracketByUserIdQuery(userId);
const [value, setValue] = useState(["", ""]);
console.log({ value })
// const [bracketState, setBracketState] = useState()
// const [bracketLoad, setBracketLoad] = useState()
// const [bracketSuccess, setBracketSuccess] = useState()
const {
data: visits,
isLoading,
isSuccess} = useGetVisitsByUserIdQuery(userId);
// const handleSearch = (async () => {
// // if (isSearching) {
// let startDate = value[0];
// let endDate = value[1];
// const { data: brackets, isLoading: isBracketsLoading, isSuccess: isBracketsSuccess } = useGetBracketsOfUserByDateQuery(userId, startDate, endDate);
// setBracketState(brackets)
// setBracketLoad(isBracketsLoading)
// setBracketSuccess(isBracketsSuccess)
// .then(() => setIsSearching(false));
// // } else if (isMounted.current) {
// // setIsSearching(false)
// // }
// })
// if (!isSearching) {
// setValue(["", ""])
// } else if (isSearching) {
// handleSearch()
// }
// useEffect(() => {
// if (isSearching) {
// const effect = async () => {
// await handleSearch();
// if (!isMounted()) return;
// setIsSearching(false);
// };
// effect();
// }
// }, [isSearching, isMounted, handleSearch]);
let bracketInfo = <div><h3>Select dates above and click run to view time card info</h3></div>
// if (bracketLoad) {
// bracketInfo = <div><h3>Loading...</h3></div>
// } else if (bracketSuccess) {
// bracketInfo =
// <div>
// <ul>
// {bracketState &&
// bracketState?.map((bracket) => (
// <TimeBracket key={bracket._id} bracket={bracket} />
// ))
// }
// </ul>
// </div>
// }
const handleClick = () => {
<DateSearch value={[value]} />
}
return (
<div>
<h1>Timecard</h1> <AddTimeBracket />
<br></br><br></br><br></br>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<fieldset width='300px'>
<legend>Select Date Range</legend>
<DateRangePicker
value={value}
onChange={(newValue) => {
setValue(newValue);
}}
renderInput={(startProps, endProps) => (
<React.Fragment>
<TextField {...startProps} />
<Box sx={{ mx: 2}}> to </Box>
<TextField {...endProps} />
</React.Fragment>
)}
/>
<Button onClick={() => handleClick()} color='primary' variant='contained'>
Run
</Button>
</fieldset>
</LocalizationProvider>
<br></br><br></br><br></br><br></br>
<>
{bracketInfo}
</>
</div>
)
}
and the RTK Query being called in a child component to avoid breaking rule of hooks:
export default function DateSearch ([value]) {
const [bracketState, setBracketState] = useState()
const [bracketLoad, setBracketLoad] = useState()
const [bracketSuccess, setBracketSuccess] = useState()
let startDate = value[0];
let endDate = value[1];
const { data: brackets, isLoading: isBracketsLoading, isSuccess: isBracketsSuccess } = useGetBracketsOfUserByDateQuery(userId, startDate, endDate);
setBracketState(brackets)
setBracketLoad(isBracketsLoading)
setBracketSuccess(isBracketsSuccess)
let bracketInfo
if (bracketLoad) {
bracketInfo = <div><h3>Loading...</h3></div>
} else if (bracketSuccess) {
bracketInfo =
<div>
<ul>
{bracketState &&
bracketState?.map((bracket) => (
<TimeBracket key={bracket._id} bracket={bracket} />
))
}
</ul>
</div>
}
}
UPDATE
So 1 step in the right direction, but still broken. To resolve the render loop, adding a ternary operator for the initial state of value to set a status (isSearching), and giving it a fall back value [Date.now()] allows the component to load. To abide the rule of hooks and only call a hook in the body of the component, I moved the RTK Query call from the onClick function and had the onClick change the component status's state to trigger an if condition. However, now, once the onClick is triggered, the component breaks since an additional hook was called in the new render when compared to the previous render. So if anyone knows a way around this (or how to better structure the whole component so it works...), I'm grateful for any advice.
export default function Timecard() {
const { user } = useContext(UserContext)
const [isSearching, setIsSearching] = useState(false)
const userId = user._id;
const [value, setValue] = useState(isSearching ? [] : [new Date(), new Date()]);
console.log({ value })
const [bracketState, setBracketState] = useState()
const [bracketLoad, setBracketLoad] = useState()
const [bracketSuccess, setBracketSuccess] = useState()
function handleSearch() {
setIsSearching(true)
}
if (isSearching) {
let startDate = value[0];
let endDate = value[1];
const { data: brackets, isLoading: isBracketsLoading, isSuccess: isBracketsSuccess } = useGetBracketsOfUserByDateQuery(userId, startDate, endDate);
setBracketState(brackets)
setBracketLoad(isBracketsLoading)
setBracketSuccess(isBracketsSuccess)
setIsSearching(false)
}
let bracketInfo = <div><h3>Select dates above and click run to view time card info</h3></div>
if (bracketLoad) {
bracketInfo = <div><h3>Loading...</h3></div>
} else if (bracketSuccess) {
bracketInfo =
<div>
<ul>
{bracketState &&
bracketState?.map((bracket) => (
<TimeBracket key={bracket._id} bracket={bracket} />
))
}
</ul>
</div>
}
return (
<div>
<h1>Timecard</h1> <AddTimeBracket />
<br></br><br></br><br></br>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<fieldset width='300px'>
<legend>Select Date Range</legend>
<DateRangePicker
value={value}
onChange={(newValue) => {
setValue(newValue);
}}
renderInput={(startProps, endProps) => (
<React.Fragment>
<TextField {...startProps} />
<Box sx={{ mx: 2}}> to </Box>
<TextField {...endProps} />
</React.Fragment>
)}
/>
<Button onClick={() => handleSearch()} color='primary' variant='contained'>
Run
</Button>
</fieldset>
</LocalizationProvider>
<br></br><br></br>
<>
{bracketInfo}
</>
</div>
)
}
2nd UPDATE
Ok, so setting the hook in the if condition breaks the rule of hooks as well. So with a bit of adjustment, the RTK call is now not breaking the rule of hooks, but cannot access the date range conditions because it is only running once... I'm at a loss of where to go.
export default function Timecard() {
const { user } = useContext(UserContext)
const [isSearching, setIsSearching] = useState(false)
const userId = user._id;
const [value, setValue] = useState(isSearching ? [] : [new Date(), new Date()]);
console.log({ value })
const [startDate, setStartDate] = useState(value[0])
const [endDate, setEndDate] = useState(value[1])
const [bracketState, setBracketState] = useState()
const [bracketLoad, setBracketLoad] = useState()
const [bracketSuccess, setBracketSuccess] = useState()
//let startDate = value[0];
//let endDate = value[1];
const { data: brackets,
isLoading: isBracketsLoading,
isSuccess: isBracketsSuccess
} = useGetBracketsOfUserByDateQuery(userId, startDate, endDate);
function handleSearch() {
setIsSearching(true)
}
if (isSearching) {
setBracketState(brackets)
setBracketLoad(isBracketsLoading)
setBracketSuccess(isBracketsSuccess)
setIsSearching(false)
}
let bracketInfo = <div><h3>Select dates above and click run to view time card info</h3></div>
if (bracketLoad) {
bracketInfo = <div><h3>Loading...</h3></div>
} else if (bracketSuccess) {
bracketInfo =
<div>
<ul>
{bracketState &&
bracketState?.map((bracket) => (
<TimeBracket key={bracket._id} bracket={bracket} />
))
}
</ul>
</div>
}
return (
<div>
<h1>Timecard</h1> <AddTimeBracket />
<br></br><br></br><br></br>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<fieldset width='300px'>
<legend>Select Date Range</legend>
<DateRangePicker
value={value}
onChange={(newValue) => {
setValue(newValue);
setStartDate(newValue[0])
setEndDate(newValue[1])
console.log(startDate, endDate)
}}
renderInput={(startProps, endProps) => (
<React.Fragment>
<TextField {...startProps} />
<Box sx={{ mx: 2}}> to </Box>
<TextField {...endProps} />
</React.Fragment>
)}
/>
<Button onClick={() => handleSearch()} color='primary' variant='contained'>
Run
</Button>
</fieldset>
</LocalizationProvider>
<br></br><br></br>
<>
{bracketInfo}
</>
</div>
)
}
I think you are going to very high lengths to either recreate
useLazyQuery (which will not run before you call a function the first time),
passing skipToken as an argument to the hook (which will make a query not run)
setting the skip option on the hook call (which will not make the query run)
Have you looked into these?

how to clearInterval if any previous setInterval is running in REACT?

I want to make a Reverse Countdown Timer in which that has an input field and when I type a number and press the "Enter" key, the countdown starts. but the problem is when I want to stop the previous interval and starts new interval with new number and press Enter key, it runs parallel with previous one.
I'm pretty new to react, can somebody give me some advice to fix the problem ?
import React, {useState} from "react";
const App = () => {
const [ctime, setctime]=useState();
const enterdown=(value)=>{
clearInterval(interval);
setctime(value);
var interval = setInterval(()=>{
value--;
if(value==0){
clearInterval(interval);
}
setctime(value);
},1000)
}
return (
<div className="wrapper">
<div id="whole-center">
<h1>
Reverse countdown for<input id="timeCount" onKeyDown={(e)=>{e.key == "Enter" ?(enterdown(e.target.value)) :null}} /> sec.
</h1>
</div>
<div id="current-time">{ctime}</div>
</div>
)
}
export default App;
use this const interval = useRef(); instead of var interval
Full working code:
Also codesandbox link: https://codesandbox.io/s/zealous-monad-thhrvz?file=/src/App.js
const App = () => {
const interval = useRef();
const [ctime, setctime] = useState();
const enterdown = (value) => {
clearInterval(interval.current);
setctime(value);
interval.current = setInterval(() => {
value--;
if (value == 0) {
clearInterval(interval.current);
}
setctime(value);
}, 1000);
};
return (
<div className="wrapper">
<div id="whole-center">
<h1>
Reverse countdown for
<input
id="timeCount"
onKeyDown={(e) => {
e.key == "Enter" ? enterdown(e.target.value) : null;
}}
/>{" "}
sec.
</h1>
</div>
<div id="current-time">{ctime}</div>
</div>
);
};
The interval variable gets created each time the App function is run.
Which is in this case each time you press Enter.
It can be fixed by placing it out of the App function.
let interval;
const App = () => {
const [ctime, setctime] = useState();
const enterdown = (value) => {
clearInterval(interval);
setctime(value);
interval = setInterval(() => {
value--;
if (value === 0) {
clearInterval(interval);
}
setctime(value);
}, 1000);
};
// ...
Another solution would be using useRef

Why does RxJS interval begin from last value but not from "0"?

I need to reset my custom timer to "0 seconds" when I click the Reset button. But when I press Start my timer continues from last value, not from "0 seconds".
const [time, setTime] = useState (0);
const [timerOn, setTimerOn ] = useState (false);
let observable$ = interval(1000);
let subscription = observable$.subscribe(result =>{
if (timerOn) {
setTime(result);
}
});
return () => subscription.unsubscribe();
}, [timerOn]);
return (
<div>
{!timerOn && (
<button onClick={() => setTimerOn(true)}>Start</button>
)}
{ time > 0 && (
<button onClick={() => setTime(0)}>Reset</button>
)}
Problem
Let's take a look at the Reset button:
<button onClick={() => setTime(0)}>Reset</button>
When you click this button it runs the following function:
() => setTime(0)
This just sets the time state back to 0. That's all. It doesn't touch the subscription to the interval observable at all. As a result, the interval subscription will continue emitting numbers in sequence which is why it appears to continue from the last value.
Solution
Your reset function will have to do more than just setting time back to 0. What it does exactly will be up to your specific use case. For example, it could end the subscription, end the subscription and create a new one, or reset the existing subscription. I've included a code example for a basic way of ending the subscription:
const {
StrictMode,
useCallback,
useRef,
useState,
} = React;
const { interval } = rxjs;
const rootElement = document.getElementById("root");
function useCounter() {
const [time, setTime] = useState(0);
const [timerOn, setTimerOn] = useState(false);
const observable$ = interval(1000);
const subscription = useRef();
const start = useCallback(() => {
setTimerOn(true);
subscription.current = observable$.subscribe((result) => {
console.log(result);
setTime(result);
});
});
const stop = useCallback(() => {
setTimerOn(false);
subscription.current.unsubscribe();
});
const reset = useCallback(() => {
setTime(0);
setTimerOn(false);
subscription.current.unsubscribe();
});
return {
time,
timerOn,
start,
stop,
reset
};
}
function Counter() {
const { time, timerOn, start, stop, reset } = useCounter();
return (
<div>
<h1>{time}</h1>
<br />
{!timerOn && <button onClick={() => start()}>Start</button>}
{time > 0 && <button onClick={() => reset()}>Reset</button>}
</div>
);
}
function App() {
return <Counter />;
}
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
rootElement
);
<div id="root"></div>
<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>
<script src="https://unpkg.com/rxjs#^7/dist/bundles/rxjs.umd.min.js"></script>

setInterval does not work properly in ReactJS

I got a problem with setInterval in ReactJS (particularly Functional Component). I want to enable autoplay slider (it means value in Slider Bar will be increased after a period of time). This function will be triggered when I click a button. But I don't know exactly how setInterval works with setValue. The console log just returns the initial value (does not change) (console.log function was called in a callback function of setInterval).
My code is below:
const [value, setValue] = useState(10);
const [playable, setPlayable] = useState(false);
useEffect(() => {
if (playable === true) {
console.log("Trigger");
const intervalId = setInterval(increaseValue, 1000);
return () => clearInterval(intervalId);
}
}, [playable]);
const increaseValue = () => {
console.log(value);
setValue(value + 1);
};
const autoPlay = () => {
setPlayable(true);
};
return (
<div>
<div style={{"width": "800px"}}>
<Slider
value={value}
step={1}
valueLabelDisplay="on"
getAriaValueText={valueText}
marks={marks}
onChange={handleChange}
min={0}
max={100}
/>
</div>
<button onClick={autoPlay}>Play</button>
</div>
)
So, I think you need to pass increaseValue function into useEffect
useEffect(() => {
if (playable === true) {
console.log("Trigger");
const intervalId = setInterval(increaseValue, 1000);
return () => clearInterval(intervalId);
}
}, [playable, increaseValue]);

Resources