const onDrawerClose = () => {
setCloseDrawer1(false);
setCloseDrawer2(false);
setData(null);
};
When some fields in the data are empty I show error message. It works good but when I close the sidebar, while it's closing I can see for 1second or so this error message since here in the code above we set data to null, how I can properly run the third setState after the previous two are completed?
You can listen when the draw1 and draw2 are closed using an useEffect like:
useEffect(() => {
if(!closeDrawer1 && !closeDrawer2) {
setData(null);
}
}, [closeDrawer1, closeDrawer2])
If there is an animation when drawers are closing you can set data after the animation happen using setTimeout.
useEffect(() => {
const drawerCloseTimeout = 1000; // 1 sec
if(!closeDrawer1 && !closeDrawer2) {
const timeout = setTimeout(() => setData(null), drawerCloseTimeout);
return () => clearTimeout(timeout);
}
}, [closeDrawer1, closeDrawer2]);
Related
I want to show notification message after 4 seconds when some state is invalid. But if during that 4 seconds it has changed and is valid now - I want to put condition in setTimeout that would check it. But the problem is that it still uses the first state value, not the changed one. One of my assumptions to fix it was making setState in the line before synchronous, but don't know how. Maybe any other ways to fix it?
useEffect(async () => {
try {
const snippetIndexResponse = await getSnippetIndex(
//some params
);
if (snippetIndexResponse !== -1) {
setSnippetIndex(snippetIndexResponse);
} else {
setSnippetIndex(null)
setTimeout(() => {
console.log(snippetIndex) <-- it logs only first state, instead wanted null
if(!snippetIndex) {
openNotificationWithIcon(
"error",
"Invalid snippet selection",
"Snippet slice shouldn't tear code blocks. Please, try again."
);
}
}, 4000)
}
} catch (err) {
setSnippetIndex(null);
openNotificationWithIcon("error", err.name, err.message);
}
}, [beginRow, endRow]);
First You can not call useEffect callback as async method.
Second for your purpose you can act as below:
let timeoutId = null;
useEffect(() => {
(async () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
try {
const snippetIndexResponse = await getSnippetIndex(
//some params
);
if (snippetIndexResponse !== -1) {
setSnippetIndex(snippetIndexResponse);
} else {
setSnippetIndex(null)
timeoutId = setTimeout(() => {
console.log(snippetIndex) <-- it logs only first state, instead wanted null
if(!snippetIndex) {
openNotificationWithIcon(
"error",
"Invalid snippet selection",
"Snippet slice shouldn't tear code blocks. Please, try again."
);
}
}, 4000)
}
} catch (err) {
setSnippetIndex(null);
openNotificationWithIcon("error", err.name, err.message);
}
})();
}, [beginRow, endRow]);
I think you could make the notification UI a component and pass the state in as a parameter. If the state changes the component will be destroyed and recreated.
And you can add the 4 second timer in useEffect() as well as cancel it in the 'return' of useEffect. If the timer fires, update some visibility flag.
Live typing this - so may contain some syntax errors...
cost myAlert = (isLoading) => {
const [isVisible, setIsVisible] = setIsVisible(false)
useEffect(()=>{
const timerId = setTimer, setIsVisible(true) when it goes off
return ()=>{cancelTimer(timerId)}
}, [isLoading, setIsVisible])
if (!isLoading && !isVisible) return null
if (isVisible) return (
<>
// your ui
</>
)
}
You may want to useCallback on setTimer so it won't cause useEffect to fire if isLoading changes -- but I think this should get you close to the solution.
useEffect(() => {
const id_1 = setTimeout(() => {
// do something
clearTimeout(id_1);
}, 500);
}, [success_failure_msg[0]]);
vs
useEffect(() => {
const id_1 = setTimeout(() => {
// do something
}, 500);
return () => clearTimeout(id_1);
}, [success_failure_msg[0]]);
What is the difference and which is the correct practice?
The correct practice is the second one that you provided. To answer your question you really have to understand why we clear the timeout.
Let's take an example of a simple react component:
const Comp = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
setCount(1); // set the count in the timeout
}, 1000);
}, [setCount]);
return (
<div>count: {count}</div>
);
}
Now this will work as long as the component is rendered. But what happens if the component gets unmounted before the timeout is resolved. The timeout will still run and you will be calling setCount of a component that is no longer in the view.
If you change it to the code you have given in your first example the problem is still the same:
useEffect(() => {
const id_1 = setTimeout(() => {
clearTimeout(id1); // this does not do anything as the timeout has already run
setCount(1); // set the count in the timeout
}, 1000);
}, [setCount]);
The clearing the timeout does not do anything and you will still have a memory leak. The only way to resolve it is to return a function from the useEffect that react will run when it unmounts the component. Within that function you can clean up the code.
useEffect(() => {
const id_1 = setTimeout(() => {
setCount(1); // set the count in the timeout
}, 1000);
// React will run this function when it unmounts and so no memory leak
return () => clearTimeout(id_1)
}, [setCount]);
both seems wrong to me, cleartimeout should be before settimetimeout
and when success_failure_msg changes two time the settimeout trigger only once.
Example
var id_1 = undefined;
useEffect(() => {
clearTimeout(id_1)
id_1 = setTimeout(() => {
success_failure_msg // shoule be test2
}, 500);
}, [success_failure_msg]);
success_failure_msg = "test";
success_failure_msg = "test2"
So I am writing a product prototype in create-react-app, and in my App.js, inside the app() function, I have:
const [showCanvas, setShowCanvas] = useState(true)
This state is controlled by a button with an onClick function; And then I have a function, inside it, the detectDots function should be ran in an interval:
const runFaceDots = async (key, dot) => {
const net = await facemesh.load(...);
setInterval(() => {
detectDots(net, key, dot);
}, 10);
// return ()=>clearInterval(interval);};
And the detectDots function works like this:
const detectDots = async (net, key, dot) => {
...
console.log(showCanvas);
requestFrame(()=>{drawDots(..., showCanvas)});
}
}};
I have a useEffect like this:
useEffect(()=>{
runFaceDots(); return () => {clearInterval(runFaceDots)}}, [showCanvas])
And finally, I can change the state by clicking these two buttons:
return (
...
<Button
onClick={()=>{setShowCanvas(true)}}>
Show Canvas
</Button>
<Button
onClick={()=> {setShowCanvas(false)}}>
Hide Canvas
</Button>
...
</div>);
I checked a few posts online, saying that not clearing interval would cause state loss. In my case, I see some strange behaviour from useEffect: when I use onClick to setShowCanvas(false), the console shows that console.log(showCanvas) keeps switching from true to false back and forth.
a screenshot of the console message
you can see initially, the showCanvas state was true, which makes sense. But when I clicked the "hide canvas" button, and I only clicked it once, the showCanvas was set to false, and it should stay false, because I did not click the "show canvas" button.
I am very confused and hope someone could help.
Try using useCallback for runFaceDots function - https://reactjs.org/docs/hooks-reference.html#usecallback
And ensure you return the setInterval variable to clear the timer.
const runFaceDots = useCallback(async (key, dot) => {
const net = await facemesh.load(...);
const timer = setInterval(() => {
detectDots(net, key, dot);
}, 10);
return timer //this is to be used for clearing the interval
},[showCanvas])
Then change useEffect to this - running the function only if showCanvas is true
useEffect(()=>{
if (showCanvas) {
const timer = runFaceDots();
return () => {clearInterval(timer)}
}
}, [showCanvas])
Update: Using a global timer
let timer // <-- create the variable outside the component.
const MyComponent = () => {
.....
useEffect(()=>{
if (showCanvas) {
runFaceDots(); // You can remove const timer here
return () => {clearInterval(timer)}
} else {
clearInterval(timer) //<-- clear the interval when hiding
}
}, [showCanvas])
const runFaceDots = useCallback(async (key, dot) => {
const net = await facemesh.load(...);
timer = setInterval(() => { //<--- remove const and use global variable
detectDots(net, key, dot);
}, 10);
return timer //this is to be used for clearing the interval
},[showCanvas])
.....
}
I want to establish a websocket connection with the server. Reconnect after 5 seconds if the connection closes. I am using React Hooks and so far achieved this
import React, { useRef, useState, useEffect } from 'react';
function App() {
const wsClient = useRef(null);
const [wsState, setWsState] = useState(true)
useEffect(() => {
wsClient.current = new WebSocket(url);
console.log("Trying to open ws");
setWsState(true)
wsClient.current.onopen = () => {
console.log('ws opened');
wsClient.current.send('{"type" : "hello"}')
};
wsClient.current.onclose = (event) => {
// Parse event code and log
setTimeout(() => {setWsState(false)}, 5000)
console.log('ws closed');
}
wsClient.current.onmessage = ((event) => {
// DO YOUR JOB
})
return () => {
console.log('ws closed');
wsClient.current.close();
}
}, [wsState]);
return (
<div className="App">
<Header />
<MainBody />
</div>
);
}
This is creating exponentially increasing number of retries when it is unable to connect with server, if I remove setTimeout and use simple setState it is working normally.
I am unable to understand the issue and also suggest what is the best practice to achieve my goal.
I'm not convinced that an effect is the best place for this. If it's application-level, it may be simpler to implement it in its own module, and bring that in, where needed.
Nevertheless, to get this to work, you should consider that you're managing two separate lifecycles: the component lifecycle, and the websocket lifecycle. To make it work as you want, you have to ensure that each state change in one aligns with a state change in the other.
First, keep in mind that your effect runs every time the dependencies in the array change. So, in your example, your effect runs every time you set wsState.
The other thing to keep in mind is that your cleanup function is called every time wsState changes, which you're doing twice in your effect (setting it to true on open, and false on close). This means that when you create a new socket, and it fails to connect, the close event fires, and it queues up a state change.
Each time it attempts to connect, it sets wsState to true (which queues a re-run of your effect), tries and fails to connect, finally setting another timeout, which updates the state to false. But, not before the effect runs again, trying to set the state to true, etc.
To fix this, start with the effect lifecycle. When should your effect run? When should it be cleaned up? A few thoughts:
The effect should run once during the first render, but not during subsequent renders
The effect should be cleaned up when the WebSocket disconnects
The effect should be re-run after a timeout, triggering a reconnect
What does this mean for the component? You don't want to include the WS state as a dependency. But, you do need state to trigger it to re-run after the timeout.
Here's what this looks like:
import React, { useRef, useState, useEffect } from 'react';
const URL = 'ws://localhost:8888';
export default function App() {
const clientRef = useRef(null);
const [waitingToReconnect, setWaitingToReconnect] = useState(null);
const [messages, setMessages] = useState([]);
const [isOpen, setIsOpen] = useState(false);
function addMessage(message) {
setMessages([...messages, message]);
}
useEffect(() => {
if (waitingToReconnect) {
return;
}
// Only set up the websocket once
if (!clientRef.current) {
const client = new WebSocket(URL);
clientRef.current = client;
window.client = client;
client.onerror = (e) => console.error(e);
client.onopen = () => {
setIsOpen(true);
console.log('ws opened');
client.send('ping');
};
client.onclose = () => {
if (clientRef.current) {
// Connection failed
console.log('ws closed by server');
} else {
// Cleanup initiated from app side, can return here, to not attempt a reconnect
console.log('ws closed by app component unmount');
return;
}
if (waitingToReconnect) {
return;
};
// Parse event code and log
setIsOpen(false);
console.log('ws closed');
// Setting this will trigger a re-run of the effect,
// cleaning up the current websocket, but not setting
// up a new one right away
setWaitingToReconnect(true);
// This will trigger another re-run, and because it is false,
// the socket will be set up again
setTimeout(() => setWaitingToReconnect(null), 5000);
};
client.onmessage = message => {
console.log('received message', message);
addMessage(`received '${message.data}'`);
};
return () => {
console.log('Cleanup');
// Dereference, so it will set up next time
clientRef.current = null;
client.close();
}
}
}, [waitingToReconnect]);
return (
<div>
<h1>Websocket {isOpen ? 'Connected' : 'Disconnected'}</h1>
{waitingToReconnect && <p>Reconnecting momentarily...</p>}
{messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
</div>
);
}
In this example, the connection state is tracked, but not in the useEffect dependencies. waitingForReconnect is, though. And it's set when the connection is closed, and unset a time later, to trigger a reconnection attempt.
The cleanup triggers a close, as well, so we need to differentiate in the onClose, which we do by seeing if the client has been dereferenced.
As you can see, this approach is rather complex, and it ties the WS lifecycle to the component lifecycle (which is technically ok, if you are doing it at the app level).
However, one major caveat is that it's really easy to run into issues with stale closures. For example, the addMessage has access to the local variable messages, but since addMessage is not passed in as a dependency, you can't call it twice per run of the effect, or it will overwrite the last message. (It's not overwriting, per se; it's actually just overwriting the state with the old, "stale" value of messages, concatenated with the new one. Call it ten times and you'll only see the last value.)
So, you could add addMessage to the dependencies, but then you'd be disconnecting and reconnecting the websocket every render. You could get rid of addMessages, and just move that logic into the effect, but then it would re-run every time you update the messages array (less frequently than on every render, but still too often).
So, coming full circle, I'd recommend setting up your client outside of the app lifecycle. You can use custom hooks to handle incoming messages, or just handle them directly in effects.
Here's an example of that:
import React, { useRef, useState, useEffect } from 'react';
const URL = 'ws://localhost:8888';
function reconnectingSocket(url) {
let client;
let isConnected = false;
let reconnectOnClose = true;
let messageListeners = [];
let stateChangeListeners = [];
function on(fn) {
messageListeners.push(fn);
}
function off(fn) {
messageListeners = messageListeners.filter(l => l !== fn);
}
function onStateChange(fn) {
stateChangeListeners.push(fn);
return () => {
stateChangeListeners = stateChangeListeners.filter(l => l !== fn);
};
}
function start() {
client = new WebSocket(URL);
client.onopen = () => {
isConnected = true;
stateChangeListeners.forEach(fn => fn(true));
}
const close = client.close;
// Close without reconnecting;
client.close = () => {
reconnectOnClose = false;
close.call(client);
}
client.onmessage = (event) => {
messageListeners.forEach(fn => fn(event.data));
}
client.onerror = (e) => console.error(e);
client.onclose = () => {
isConnected = false;
stateChangeListeners.forEach(fn => fn(false));
if (!reconnectOnClose) {
console.log('ws closed by app');
return;
}
console.log('ws closed by server');
setTimeout(start, 3000);
}
}
start();
return {
on,
off,
onStateChange,
close: () => client.close(),
getClient: () => client,
isConnected: () => isConnected,
};
}
const client = reconnectingSocket(URL);
function useMessages() {
const [messages, setMessages] = useState([]);
useEffect(() => {
function handleMessage(message) {
setMessages([...messages, message]);
}
client.on(handleMessage);
return () => client.off(handleMessage);
}, [messages, setMessages]);
return messages;
}
export default function App() {
const [message, setMessage] = useState('');
const messages = useMessages();
const [isConnected, setIsConnected] = useState(client.isConnected());
useEffect(() => {
return client.onStateChange(setIsConnected);
}, [setIsConnected]);
useEffect(() => {
if (isConnected) {
client.getClient().send('hi');
}
}, [isConnected]);
function sendMessage(e) {
e.preventDefault();
client.getClient().send(message);
setMessage('');
}
return (
<div>
<h1>Websocket {isConnected ? 'Connected' : 'Disconnected'}</h1>
<form onSubmit={sendMessage}>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button type="submit">Send</button>
</form>
{messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
</div>
);
}
Here is what I use:
const [status, setStatus] = useState('closing')
let socket
useEffect(() => {
if (!condition1) {
return
}
if (socketStatus == 'closing') {
connectSocket()
setSocketStatus('opening')
}
}, [socketStatus])
function connectSocket() {
socket = new WebSocket('ws://...');
socket.addEventListener('open', function (m) {
newSocket.send('...')
});
socket.onmessage = function (e) {
log(e.data)
}
socket.onclose = function (e) {
setTimeout(() => {
setSocketStatus('closing')
}, 2000);
};
socket.onerror = function (err: any) {
socket.close();
};
}
I want to have all of the setTimeOut cleared when the component is unmounted.
Even though I have use clearTimeOut as a clean up function but the error still persis: "Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function"
useEffect(() => {
const timeOut = {timeout1: ()=>setTimeout(() => setProgress((preV) => preV + 15), [
550,
]),timeout2 : ()=> setTimeout(() => setMessage("All most done"), [500])}
timeOut.timeout1();
timeOut.timeout2();
return () => {
clearTimeout(timeOut.timeout1);
clearTimeout(timeOut.timeout2);
};
}, [progress,message]);
Does anyone know how to solve this issue? Any help would be appreciated!
timeout1 isn't storing the returned value of setTimeout which is the timerId but its storing the reference of the function that executes timeout
You could write your code in a way that it executes the timeouts immediately using Immediately invoked functions so that timeout1 and timeout2 have timerIds
useEffect(() => {
const timeOut = {
timeout1: (()=>setTimeout(() => setProgress((preV) => preV + 15), 550))(),
timeout2 : (()=> setTimeout(() => setMessage("All most done"), 500))()
}
return () => {
clearTimeout(timeOut.timeout1);
clearTimeout(timeOut.timeout2);
};
}, [progress,message]);
however you could simply run the timeouts without writing them as IIFE
useEffect(() => {
const timeOut = {
timeout1: setTimeout(() => setProgress((preV) => preV + 15), 550),
timeout2 : ()=> setTimeout(() => setMessage("All most done"), 500)
}
return () => {
clearTimeout(timeOut.timeout1);
clearTimeout(timeOut.timeout2);
};
}, [progress,message]);
How about to use a local variable that keeps track of whether the component is mounted or not.
useEffect(() => {
let run = true;
const timeOut = {
timeout1: () => setTimeout(() => setProgress((preV) => preV + 15), [550]),
timeout2: () => setTimeout(() => setMessage('All most done'), [500]),
};
if (run) {
timeOut.timeout1();
timeOut.timeout2();
}
return () => {
clearTimeout(timeOut.timeout1);
clearTimeout(timeOut.timeout2);
run = false;
};
}, [progress, message]);