Latest function callback is not caught in React - javascript

In the example bellow, Child component calls onFinish callback 5 seconds after clicking on button. The problem is that onFinish callback can change in those 5 seconds, but the it will call the last caught one.
import React, { useState } from "react";
const Child = ({ onFinish }) => {
const [finished, setFinished] = useState(false);
const finish = async () => {
setFinished(true);
setTimeout(() => onFinish(), 5000);
};
return finished ? (
<p>Wait 5 seconds and increment while waiting.</p>
) : (
<button onClick={finish}>Click here to finish</button>
);
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<Child onFinish={() => alert(`Finished on count: ${count}`)} />
</>
);
}
The workaroud for this one is to replace finish with the following:
const cb = useRef();
cb.current = onFinish;
const finish = async () => {
setFinished(true);
setTimeout(() => cb.current(), 5000);
};
Is there a better approach to update the callback to the latest one?

Yes, you can check the current state and compare it with prev state like
setFinished((prevState) => newState)

Related

How can I start / stop setInterval?

I've tried different ways, but It doesn't works.
[...]
const [automatic, setAutomatic] = useState(false);
[...]
var startAuto;
useEffect(() => {
if (!automatic) {
console.log("stop");
clearInterval(startAuto);
} else {
startAuto = setInterval(() => {
changeQuestion("+");
}, 5 * 1000);
}
}, [automatic]);
[...]
<Button
onPress={() => setAutomatic(!automatic)}
title="turn on/off"
/>
[...]
It works when I put a setTimeout outside the useEffect, that way:
setTimeout(() => { clearInterval(startAuto); alert('stop'); }, 10000);
But I want to have a button to start / stop
Your var startAuto; is redeclared on each render, and since changing the state causes a re-render, it never holds the reference to the interval, which is never cleared.
Use the useEffect cleanup function to clear the interval. Whenever automatic changes, it would call the cleanup (if returned by the previous invocation), and if automatic is true it would create a new interval loop, and return a new cleanup function of the current interval.
useEffect(() => {
if(!automatic) return;
const startAuto = setInterval(() => {
changeQuestion("+");
}, 5 * 1000);
return () => {
clearInterval(startAuto);
};
}, [automatic]);
Working example:
const { useState, useEffect } = React;
const Demo = () => {
const [automatic, setAutomatic] = useState(false);
const [question, changeQuestion] = useState(0);
useEffect(() => {
if(!automatic) return;
const startAuto = setInterval(() => {
changeQuestion(q => q + 1);
}, 5 * 100);
return () => {
clearInterval(startAuto);
};
}, [automatic]);
return (
<div>
<button
onClick={() => setAutomatic(!automatic)}
>
turn {automatic ? 'off' : 'on'}
</button>
<p>{question}</p>
</div>
);
}
ReactDOM
.createRoot(root)
.render(<Demo />);
<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="root"></div>
For example, you can check and use this hook:
https://usehooks-ts.com/react-hook/use-interval
export default function Component() {
// The counter
const [count, setCount] = useState<number>(0)
// Dynamic delay
const [delay, setDelay] = useState<number>(1000)
// ON/OFF
const [isPlaying, setPlaying] = useState<boolean>(false)
useInterval(
() => {
// Your custom logic here
setCount(count + 1)
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null,
)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDelay(Number(event.target.value))
}
return (
<>
<h1>{count}</h1>
<button onClick={() => setPlaying(!isPlaying)}>
{isPlaying ? 'pause' : 'play'}
</button>
<p>
<label htmlFor="delay">Delay: </label>
<input
type="number"
name="delay"
onChange={handleChange}
value={delay}
/>
</p>
</>
)
}

Why my sleep function doesn't make the React application freeze?

Why my sleep function doesn't make the React application freeze? Here's my code:
import React from "react";
import "./App.css";
function App() {
const [count, setCount] = React.useState(0);
(async () => {
const sleep = async (miliseconds: number) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("");
}, miliseconds);
});
};
await sleep(5000);
console.log("hey");
})();
return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>+</button>
</div>
);
}
export default App;
So, I have an IIFE sleep function inside the component that is supposed to execute before every render. But when I click on the increment button of my counter, the DOM being updated immediately without waiting for my sleep function to finish its execution. What's wrong with it? If I use for loop to freeze the app everything works as expected but the sleep function implemented with promise doesn't cause my app freeze.
What this block of code does:
(async () => {
const sleep = async (miliseconds: number) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("");
}, miliseconds);
});
};
await sleep(5000);
console.log("hey");
})();
is it creates a Promise that resolves after 5 seconds. That's it. The Promise isn't used anywhere, and so it isn't connected to anything in the rest of the code.
function App() {
const [count, setCount] = React.useState(0);
// here, create a Promise that resolves after 5 seconds, and don't do anything with it
return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>+</button>
</div>
);
}
The App's return still executes immediately when App is called, so there's no delay before it renders.
If you wanted to add a render delay, conditionally render the component and set a state after 5 seconds.
function App() {
const [count, setCount] = React.useState(0);
const [render, setRender] = React.useState(false);
React.useEffect(() => {
setTimeout(() => {
setRender(true);
}, 5000);
}, []);
return !render ? null : (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>+</button>
</div>
);
}
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<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 class='react'></div>

usage of React.memo() inside components with prop functions

import React, { useState } from 'react'
const App = () => {
const [count, setCount] = useState<number>(0);
const [otherCount, setOtherCount] = useState<number>(0);
const increment = () => {
setCount((pre) => {
return pre + 1
})
}
const decrease = () => {
setOtherCount((pre) => {
return pre - 1
})
}
return (
<>
<DecrementComponent decrease={decrease} />
<br />
<br />
<IncrementComponent increment={increment} />
</>
)
}
const DecrementComponent = React.memo(({ decrease }: { decrease: () => void; }) => {
console.log("DecrementComponent");
return (
<div>
<button onClick={decrease}>Decrement</button>
</div>
)
})
const IncrementComponent = React.memo(({ increment }: { increment: () => void; }) => {
console.log("IncrementComponent");
return (
<div>
<button onClick={increment}>Increment</button>
</div>
)
})
export default App
**React.memo(), although I used React.memo(), when I clicked increment or decrement functions, two components were rendered.
But I think one component shoud be rendered in this senerio. Why were two component rendered ?
**
React.memo can only help if the props don't change. But the increment and decrement functions change on every render, so the props are always changing. You will need to memoize those functions so that they don't change.
const increment = useCallback(() => {
setCount((pre) => {
return pre + 1
});
}, []);
const decrement = useCallback(() => {
setCount((pre) => {
return pre - 1
});
}, []);

React function doesn't updated with state

I'm trying to execute a function that is using react state but when state changes the function doesn't updates with the state value.
const {useState, useEffect} = React;
function Example() {
const [count, setCount] = useState(0);
const testFunction = function(){
setInterval(() => {
console.log(count)
}, 3000)
}
useEffect(() => {
let fncs = [testFunction];
fncs.forEach(fnc => fnc.apply(this));
}, [])
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.getElementById('other-div').innerHTML = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
ReactDOM.render( <Example />, document.getElementById('root') );
Exmaple:
https://jsfiddle.net/bzyxqkwt/4/
just so you understand, im passing functions to another component and he execute those functions on some event like this
fncs.forEach(fnc => fnc.apply(this, someEventParams));
I'm just guessing that the setInterval is just for demo purposes and needs to be stopped/removed, but basically useEffect captures the initial state. So if you want to catch inside the inner function updated state, then you should pass all the values that you need (in this case count parameter). Something like:
const testFunction = function(){
// setInterval(() => {
console.log(count); // it should log the updated value
// }, 3000)
}
useEffect(() => {
let fncs = [testFunction];
fncs.forEach(fnc => fnc.apply(this));
}, [count]); // <-- this makes the trick

clearInterval does not get called inside React.useEffect hook

My React game has a <Clock/> component to keep track of the time.
The timer should stop when the game is paused.
I am using Redux to manage the play/pause state, as well as the elapsed time.
const initialState = { inProgress: false, timeElapsed: 0 }
The inProgress state is handled by a button on another component, which dispatches an action to update the store (for the inProgress value only).
The <Clock/> component increments timeElapsed in its useEffect hook with setInterval. Yet it does not clear.
import React from 'react';
import { connect } from 'react-redux';
const Clock = ({ dispatch, inProgress, ticksElapsed }) => {
React.useEffect(() => {
const progressTimer = setInterval(function(){
inProgress ? dispatch({ type: "CLOCK_RUN" }) : clearInterval(progressTimer);
}, 1000)
}, [inProgress]);
return (
<></>
)
};
let mapStateToProps = ( state ) => {
let { inProgress, ticksElapsed } = state.gameState;
return { inProgress, ticksElapsed };
}
export default connect(
mapStateToProps,
null,
)(Clock);
Inside setInterval, when inProgress is false, I would expect clearInterval(progressTimer) to stop the clock.
Also, there is another issue where leaving out the [inProgress] in the useEffect hook causes the timer to increment at ridiculous rates, crashing the app.
Thank you.
The inProgress is a stale closure for the function passed to setInterval.
You can solve it by clearing the interval in the cleanup function:
const Clock = ({ dispatch, inProgress, ticksElapsed }) => {
React.useEffect(() => {
const progressTimer = setInterval(function () {
inProgress && dispatch({ type: 'CLOCK_RUN' });
}, 500);
return () =>
//inprogress is stale so when it WAS true
// it must now be false for the cleanup to
// be called
inProgress && clearInterval(progressTimer);
}, [dispatch, inProgress]);
return <h1>{ticksElapsed}</h1>;
};
const App = () => {
const [inProgress, setInProgress] = React.useState(false);
const [ticksElapsed, setTicksElapsed] = React.useState(0);
const dispatch = React.useCallback(
() => setTicksElapsed((t) => t + 1),
[]
);
return (
<div>
<button onClick={() => setInProgress((p) => !p)}>
{inProgress ? 'stop' : 'start'}
</button>
<Clock
inProgress={inProgress}
dispatch={dispatch}
ticksElapsed={ticksElapsed}
/>
</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>

Categories