I am writing a simple timer app and using setInterval for the first time. The project is in react typescript and I use the useReducer hook to manage state.
The project requires two separate timers, session and break, which may explain some of the code. The project also requires one button to start and stop the timer, hence I am using a single function.
I have redacted my reducer and types as the problem, as i understand, does not involve them, but some simple use of setInterval that I just don't get yet.
When the startStopTimer function is called a second time, the intervalID is undefined, even though it is declared globally.
const App = () => {
const [timerState, dispatch] = useReducer(timerStateReducer, initialTimerState
);
let intervalID: NodeJS.Timer;
const startStopTimer = () => {
let name: string = timerState.session.count !== 0 ? 'session' : 'break';
if (timerState[name].running) {
console.log('stopping timer' + intervalID);
//reducer sets running boolean variable to false
dispatch({type: ActionKind.Stop, name: name});
clearInterval(intervalID);
} else {
//reducer sets running boolean variable to true
dispatch({type: ActionKind.Start, name: name});
intervalID = setInterval(() => {
dispatch({type: ActionKind.Decrease, name: name});
}, 1000);
}
};
return (
//redacted JSX code
<button onClick={startStopTimer}>Start/Stop</button>
)
I have tried passing onClick as an arrow function (rather than a reference, i think?) and it behaves the same. I tried simplifying this with useState, but came across a whole 'nother set of issues with useState and setInterval so I went back to the useReducer hook.
Thanks!
Related
EDIT I used the wrong term in the title and question. I did not mean a global variable, but to instead declare timeoutID inside of the showNotification function.
I'm on my first week of testing Redux. I'm wondering if there is a more elegant / less hacky solution to using a glodal variable for the timeoutID? clearTimeout is used to guarantee that the last added notification is always shown for the full desired time, even if it would be added before the previous notification was set to "empty".
actionCreator.js
import { addQuote } from "./addQuote"
import { showNotification } from "./showNotification"
export const actionCreator = (quote, time) => {
return dispatch => {
dispatch(addQuote(quote))
dispatch(showNotification(quote, time))
}
}
showNotification.js
let timeoutID = null
export const showNotification = (newQuote, time) => {
const message = `A new quote by ${newQuote.author} was added.`
return dispatch => {
dispatch({ type: 'SHOW_NOTIFICATION', data: message })
clearTimeout(timeoutID)
timeoutID = setTimeout(() => {
dispatch({ type: 'SHOW_NOTIFICATION', data: '' })
}, time * 1000)
}
}
notificationReducer.js
const initState = {
notification: ''
}
const notificationReducer = (state=initState, action) => {
switch (action.type) {
case 'SHOW_NOTIFICATION':
const message = action.data
return {
...state,
notification: message
}
default:
return {
...state
}
}
}
export default notificationReducer
It's not global, it's a closure, and that's one of the core principles in a module world (which was invented to circumvent having to make use of the global namespace!).
If it were global you could use the variable in any other JS file without ever explicitly importing it.
In actionCreator.js, which does import { showNotification } from "./showNotification", try to console.log(timeoutID) and you'll get undefined.
Closures are really nothing complicated; it just means that any function when declared will "remember" any local variables that were known ("in scope" is the more technically correct term for "known") at the point of the function's declaration, no matter when the function is called, or who calls it. Those variables known to a function via this mechanism are called closures.
There is not only nothing wrong with programming this way; it moreso is state of the art and in contrast to other far more verbose and lengthy solutions like passing parameters around, the most elegant way to solve the problem.
I build a simple todo app with a react with an array of todos:
const todos = [description: "walk dog", done: false]
I use the following two states:
const [alltodos, handleTodos] = useState(todos);
const [opencount, countOpen] = useState(alltodos.length);
This is the function which counts the open todos:
const countTodos = () => {
const donetodos = alltodos.filter((item) => {
return !item.done;
});
countOpen(donetodos.length);
};
When I try to add a new todo, I also want to update the opencount state with the countTodos function.
const submitTodo = (event) => {
event.preventDefault();
const data = {
description: todo,
done: false,
};
handleTodos([...alltodos, data]);
console.log(alltodos);
countTodos();
};
This does not work as expected, the when I run console.log(alltodos) it will show an empty array. The function itself works, but it seems to have a "delay", I guess based on the async nature of the useState hook.
I tried to pass the countTodos function as callback like this, since I have seen something similar in class based components.
handleTodos([...alltodos, data], () => {
countTodos();
});
I get the following error:
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
How can I solve this problem? What is the best way for me to update the state based on another state?
I think you should useEffect, (clearly stated on the log message ). this is an example :
useEffect(()=>{
const donetodos = alltodos.filter((item) => {
return !item.done;
});
countOpen(donetodos.length);
//countTodos();
},[alltodos]];
You can refer to the documentation : https://reactjs.org/docs/hooks-effect.html
Here is an example : https://codesandbox.io/s/react-hooks-useeffect-forked-4sly8
I have a React component with a state variable jobs. When the state variable ready is true, it should start executing jobs by a Web Worker (one at a time).
import React, { useEffect, useRef } from 'react'
// create webworker
const job_worker = new Worker("worker.bundle.js", { type: "module" });
function App() {
const [jobs, set_jobs] = React.useState([
{ ... },
{ ... },
])
const [ready, set_ready] = React.useState(false)
// start worker loop
useEffect(() => {
const worker_loop = async () => {
setTimeout(async () => {
// check if ready to execute a job
if (ready) { // <== suffers from 'stale closure'
// grab a job
const job = jobsRef.current.find(j => !j.done)
// listen for webworker results
job_worker.onmessage = (e) => {
console.log("received response from webworker: '", e.data, "'")
// SET RESULT IN JOB
// job is handled by worker; now start the worker_loop again
return worker_loop()
}
// post job to worker
job_worker.postMessage({job: job})
return // do not continue; 'onmessage' will continue the loop
}
return worker_loop()
}, 1000)
}
// start worker_loop
worker_loop()
}, [])
return (
<div>
{/* code to add jobs and set ready state */}
</div>
);
}
I tried to do this by using an (infinite) worker_loop, which is started when the React component mounts (using useEffect). The loop kinda works, but the ready variable inside the worker_loop stays at the initial state value (known as the 'stale closure' problem). Probably the same for the jobs state variable.
I've already tried to use 'createRef' as suggested here. But the problem persists. Also I feel like there is a much simpler solution.
Is there a better way to handle 'jobs' in a React-state variable? Some sort of 'job-runner process/function' with access to the React component. By the way, I am not obliged to use WebWorker.
Thanks for the comments!
It indeed makes more sense to control the jobs outside React. I solved it by creating a global state using #hookstate/core. This makes it possible to access and control the state outside of React. Much cleaner code!
In my application, I'm using a dispatch from useReducer hook on click of a button and in the same function I'm using a setTimeout of 2 seconds. But when I store the data using dispatch of usereducer then I'm not getting updated value in setTimeout function.
I cannot share original code, but sharing a snippet of another demo app where this issue occurs.
const initialData = { data: "ABC" };
function reducer(state = initialData, action) {
switch (action.type) {
case "STORE":
return {
...state,
data: action.payload
};
default:
return state;
break;
}
}
function Demo() {
const [state, dispatch] = React.useReducer(reducer, initialData);
console.log("Render : ",state.data); //Will be executed on each rendering
const handleClick = () => {
dispatch({
type: "STORE",
payload: state.data + parseInt(Math.random() * 10)
});
setTimeout(() => {
console.log("ButtonClick : ",state.data); //Will be executed after 2 seconds of dispatching.
}, 2000);
};
return <button onClick={handleClick}>{state.data}</button>;
}
ReactDOM.render(<Demo />, document.getElementById("app"));
In above example I'm storing data in reducer using dispatch, and I'm calling console.log("ButtonClick") on Button Click after 2 seconds but even after 2 seconds, I'm not getting updated data in console. But in console.log("Render") I'm getting updated data.
Live example on : https://codepen.io/aadi-git/pen/yLJLmNa
When you call
const handleClick = () => {
dispatch({
type: "STORE",
payload: state.data + parseInt(Math.random() * 10)
});
setTimeout(() => {
console.log("ButtonClick : ",state.data); //Will be executed after 2 seconds of dispatching.
}, 2000);
};
this is what happens:
Run dispatch with an object to store some data. This function is executed asynchronically, so the result is not available immediately.
Register a timeout handler, which logs the current value of state.data to the console. Since the preceding dispatch is still working in progress, the value of state.data is still the old one.
This means you can not log a new dispatched value by running console.log after your dispatch call because you can not see into the future. You can only log the new data after a re-render of your component due to changing state. Then you can and should use
React.useEffect(() => {
console.log(state.data);
}, [state.data]);
Some more explanations about setTimeout and why console.log logs old values within it
You use
setTimeout(() => {
console.log("ButtonClick : ", state.data);
}, 2000);
and this is equivalent to
const callback = () => console.log("ButtonClick : ", state.data);
setTimeout(callback, 2000);
In the first line you create a function (named callback here), which prints some text. This text consists of a simple string and the value of state.data. Therefore this function has a reference to the variable state.data. The function in combination with the reference to the outer state is called closure and this closure ensures, that the value state.data is kept alive as long as the function lives (is not binned by the garbage collector).
In the second line setTimeout is called with this function. Simplified this means a task is stored somewhere which states, that exactly this function has to be executed after the given timeout. So as long as this task is not done, the function stays alive and with it the variable state.data.
In the meantime long before the task is handled, the new action is dispatched, new state calculated and Demo re-rendered. With that a new state.data and a new handleClick is created. With the new creation of handleClick also a new function is created which is passed to setTimeout. This whole handleClick function is then passed as onClick handler for the button element.
The re-render is now over, but the task from before is still pending. Now when the timeout duration ends (long after re-rendering the component) the task is taken from the task queue and executed. The task still has reference to the function from the render before and this function still has reference to the value state.data from the render before. So the value logged to the console is still the old value from the render before.
So I've always thought of arrow functions to be a new better and version of normal js functions until today. I was following a tutorial on how to use firestore to store data when I came across a problem that made realise the two are different and work in a weird way.
His code looked like this:
//component
function Todos() {
const [ todo, setTodo ] = useState('');
const ref = firestore().collection('todos');
// ...
async function addTodo() {
await ref.add({ title: todo, complete: false});
setTodo('');
}
// ...
}
My code looked like this:
//component
const Todos = () => {
const ref = firestore().collection('todos');
const [todo, setTodo] = useState('');
const addTodo = async () => {
const res = await ref.add({ title: todos, complete: false });
setTodo('');
};
};
Now his version worked, while mine didn't.
After changing my code to look like his, it worked. But the weird thing i realised was this: after clicking on the button that invoked that function for the first time (with his function), i changed the code back to mine and it worked the second time. I did some reading on the two functions but i couldn't get to reasoning behind why this happened.
Arrow functions and normal function are not equivalent.
Here is the difference:
Arrow function do not have their own binding of this, so your this.setState refer to the YourClass.setState.
Using normal function, you need to bind it to the class to obtain Class's this reference. So when you call this.setState actually it refer to YourFunction.setState().
Sample Code
class FancyComponent extends Component {
handleChange(event) {
this.setState({ event }) // `this` is instance of handleChange
}
handleChange = (event) => {
this.setState({ event }) // `this` is instance of FancyComponent
}
}