I ultimately want to display a modal to the user if a certain condition is met after X seconds of the app execution.
However, I keep getting a warning about setting a timer for a long period of time (a few minutes) on android, but my timer only has a few seconds.
const surveyCheckTime = 3000; // X = 3 seconds
const App = () => {
// First Time Modal State
const [surveyVisibility, setSurveyVisibility] = useState(false);
const handleSurveyOpen = () => {
setSurveyVisibility(true);
};
const handleSurveyClose = () => {
setSurveyVisibility(false);
};
useEffect(() => {
const timer = setTimeout(() => {
console.log('0:FROM_APP: Checking for condition: ');
getData().then(
keyValue => {
console.log('0:FROM_APP: Completion status: ', keyValue);
if (keyValue === 'false' || keyValue === undefined) {
handleSurveyOpen();
}
},
error => {
// console.log('Survery_Error: ', error);
},
);
}, surveyCheckTime);
return () => clearTimeout(timer);
}, []);
return (
<View>
...
<SurveyModal
surveyVisibility={surveyVisibility}
handleSurveyClose={handleSurveyClose}
/>
</View>
)
getData() is an async function returning keyValue as promise.
export const getData = async (myKey = 'alreadyOpened') => {
try {
const value = await AsyncStorage.getItem(`#${myKey}`);
if (value !== null) {
// value previously stored
// console.log('FROM__getData: Stored previously: ', value);
return value;
}
} catch (e) {
// error reading value
console.log('ERROR:FROM__getData: Cannot read: ', e);
return 'fasle';
}
};
All is good and everything works as intended but I keep getting this warning with a different last line everytime:
(Saw setTimeout with duration Y ms) Y is way higher than my X value
There is this question but it's not exactly firebase related in my case. (Although I do use firebase to fetch data inside another component but it shouldn't be the issue) IT IS THE ISSUE
I've looked into this, but most of the solutions either change the "node_modules" or disable the warning and the topic is pretty much outdated.
It would be best If there's a more efficient solution of handling timeouts.
EDIT: "firebase": "7.13.1" is what's causing the issue when fetching data
I was using a deprecated integration of firebase which was causing this issue, (web config and not .json file)
Related
I have a websocket server that sends an object containing some hashes every 15 seconds. When the client receives a hash, I want to check with my current hash. If they differ, I want to make a call to an API to fetch new data.
The socket is working and sending the hash correctly. If the data updates on the server I get a different hash. My problem is that the hash variable I use to store the current hash is not updated correctly.
I have disabled the socket listening in my component, just to make sure that that is not the problem. Instead I have added a setInterval to mimik the socket update.
This is my code (socked code disabled but left as a comment):
import { useCallback, useEffect, useState } from "react";
import { useAuth, useSocket } from "../utils/hooks";
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState<QuestionListModel[]>([]);
const { user } = useAuth();
const { socket } = useSocket();
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response: ApiResponse | boolean = await getQuestionLists(user?.token);
if (typeof response !== "boolean" && response.data) {
setQuestionLists(response.data);
}
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = useCallback((newHash: Hash) => {
console.log("called update");
let shouldUpdate = false;
let originalHash = { ...hash };
let updatedHash = { ...newHash };
console.log("new: ", newHash);
console.log("stored: ", originalHash);
if (hash.questionList !== newHash.questionList) {
console.log("was not equal");
updatedHash = { ...updatedHash, questionList: newHash.questionList}
shouldUpdate = true;
}
if (shouldUpdate) {
console.log("trying to set new hash: ", updatedHash);
setHash(updatedHash);
fetchQuestionLists();
}
}, [hash]);
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, 15000)
}, []);
return (
<>
... Things here later ...
</>
);
};
export default Admin;
After the initial render, and waiting two interval cycles, this is what I see in the console:
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
You can see that stored is empty. That leads me to believe that setHash(updatedHash); never runs for some reason. Why is that?
Having hacked about with this in codepen here: https://codesandbox.io/s/as-prop-base-forked-l3ncvo?file=/src/Application.tsx
This seems to me to be a closure issue as opposed to a React issue. If you have a look in the dev tools, you'll see the state of the component is doing what you're expecting it to. The issue is that the console log is not.
useEffect is only ever going to use an old version of update, so the console won't log what you're expecting. If you add update to the dependency array (and add a clean up so we don't end up with tonnes of intervals) you'll get what you're looking for. Can be seen in the linked codepen.
I think the issue in on this line :
socket?.on('aHash', (hash) => update(hash));
maybe when you register a listener, it keeps the first value of update only,
can you please share useSocket?
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
Include setHash in your dependency list et voilà
EDIT: Or well, you should include these dependencies in all your useCallback/useEffect hooks since the reference will be lost whenever the component updates. You always have to include all dependencies in the dependency list not to get unpredictable behavior.
use setState(prevValue => {}) to get the the preferred effect. Also, if you running in a Strict mode this will fire the setState twice.
Here is how the code should look like:
import { useCallback, useEffect, useState } from "react";
import { faker } from '#faker-js/faker';
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState([]);
const [ hash, setHash ] = useState({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response = {data: {hash: 'asdf-1234'}}
setQuestionLists(response.data);
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = (newHash) => {
console.log("called update");
setHash(oldHash => {
console.log('old hash: ', oldHash);
console.log('new hash', newHash);
if (JSON.stringify(oldHash) !== JSON.stringify(newHash)) {
return newHash
}
})
};
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: faker.random.numeric(36)});
}, 15000)
}, []);
return (
<>
<h2>Hash</h2>
{JSON.stringify(hash)}
</>
);
};
export default Admin;
In both cases (socket & interval) the issue is that you need to re-define the callback functions with the new context of the variables in the scope, whenever something changes. In this case you will probably need to put "update" (and whatever other variable you need to "watch") inside the dependancy array of the useEffect.
Ive had a similar issues. Here is how I ended up defining socket callback that updates correctly. Notice that I added the save function (just a function that saves the state into the useState). Also, you need to return a clean up function to turn the socket callback off when the component unmounts. This way every time anything changes in the dependancy array, the hook re-runs and recreates that callback with the new information.
React.useEffect(() => {
socketRef?.current?.on(
'private_message_sent_to_client',
(data: IMessageResult) => {
savePrivateMessages(data);
}
);
return () => {
socketRef?.current?.off('private_message_sent_to_client');
};
}, [meta, selectedChatId, savePrivateMessages]);
And here is an example for you
React.useEffect(() => {
socket?.on('aHash', (hash) => update(hash));
return () => {
socket?.off('aHash')
};
}, [update, hash]);
I am setting up a setInterval with some VideoPlayer iFrame. Every 10 seconds or so, I need to call an API to update back-end with new state of the video played.
I want to have another check on client, if the data from previous state is same as new one (This case occurs when the video is paused for longer duration) I don't want to call the back-end API.
const [coveredArray, setCoveredArray] = useState([]);
const intervalPeriod = 5000;
const interval = useRef();
useEffect(() => {
if (typeof window !== "undefined" && isOpen && player) {
console.log('SUB');
// Setup an interval to update watch time
interval.current = window.setInterval(async () => {
const totalCoveredArray = await player?.api.getTotalCoveredArray();
setCoveredArray((prevCoveredArray) => {
console.log('1', prevCoveredArray);
console.log('2', totalCoveredArray);
if (!isEqual(prevCoveredArray, totalCoveredArray)) {
console.log('New');
return totalCoveredArray;
} else {
console.log('Same');
return prevCoveredArray;
}
});
}, intervalPeriod);
return () => {
// Clear interval on exit
console.log('UN-SUB')
setCoveredArray([]);
return window.clearInterval(interval.current)
};
}
}, [isOpen, player]);
The problem with this code is, prevCoveredArray and totalCoveredArray are same everytime the interval is run. How is previousState similar to newState (which is not set yet)?
This is logged in console:
Any help/pointer would be helpful, thanks.
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.
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();
};
}
In my React-App (create-react-app) I use a selector (created with reselect) to compute derived data from stored data.The Problem is, the selector takes a long time to compute. I would like to show a spinner (or a message) on the user interface. But each time the selector is recomputed the ui freezes.
I read a lot of stuff (Web Worker, requestIdleCallback, requestAnimationFrame) and try to make my own React hook but I am stuck. I cannot use the selector in callbacks.
The searched result is simply to get the ui refreshed before the selector is recomputed.
That's my solution. I don't know if it's "good" or if it breaks some rules of react or reselect, but it works. Maybe you can assess that?The code is simplified to improve readability.
The idea is, the selector returns a Promise and I call requestAnimationFrame before computing the data to get a chance to refresh the ui.
selector.js:
const dataSelector = state => state.data;
export const getDataComputedPromise = createSelector([dataSelector], (data) => {
const compute = function () {
return new Promise((resolve) => {
// heavy computing stuff
resolve(computedData);
});
};
return new Promise((resolve) => {
let start = null;
let requestId = null;
function step (timestamp) {
if (!start) {
start = timestamp;
window.cancelAnimationFrame(requestId);
requestId = window.requestAnimationFrame(step);
return;
};
compute().then(freeResources => {
window.cancelAnimationFrame(requestId);
resolve(freeResources);
});
}
requestId = window.requestAnimationFrame(step);
});
});
myPage.js
const MyPage = ({ someProps }) => {
...
const dataComputedPromise = useSelector(getDataComputedPromise);
const [dataComputed, setDataComputed] = useState([]);
const [computeSelector, setComputeSelector] = useState(false);
useEffect(() => {
setComputeSelector(true);
}, [data]);
useEffect(() => {
dataComputedPromise.then(dataComputed => {
setDataComputed(dataComputed);
setComputeSelector(false);
});
}, [dataComputedPromise]);
...
return <div>
<Loader active={compueSelector}>Computing data...</Loader>
</div>;
};
export default MyPage;