I need periodically fetch data and update it to the screen.
I have this code:
const [temperature, setTemperature] = useState();
useEffect(() => {
fetch("urlToWeatherData")
.then(function(response) {
if (response.status !== 200) {
console.log(
"Looks like there was a problem. Status Code: " + response.status
);
return;
response.json().then(function(data) {
console.log(data[0].temperature);
setTemperature(data[0].temperature);
});
})
.catch(function(err) {
console.log("Fetch Error :-S", err);
});
}, [] );
So, is there any neat way to run it every 15 seconds, in example?
Thanks!
Wrap it in an interval, and don't forget to return a teardown function to cancel the interval when the component unmounts:
useEffect(() => {
const id = setInterval(() =>
fetch("urlToWeatherData")
.then(function(response) {
if (response.status !== 200) {
console.log(
"Looks like there was a problem. Status Code: " + response.status
);
return;
response.json().then(function(data) {
console.log(data[0].temperature);
setTemperature(data[0].temperature);
});
})
.catch(function(err) {
console.log("Fetch Error :-S", err);
});
), 15000);
return () => clearInterval(id);
}, []);
Just to give a different approach, you can define a custom hook for extracting this functionality into a reusable function:
const useInterval = (callback, interval, immediate) => {
const ref = useRef();
// keep reference to callback without restarting the interval
useEffect(() => {
ref.current = callback;
}, [callback]);
useEffect(() => {
// when this flag is set, closure is stale
let cancelled = false;
// wrap callback to pass isCancelled getter as an argument
const fn = () => {
ref.current(() => cancelled);
};
// set interval and run immediately if requested
const id = setInterval(fn, interval);
if (immediate) fn();
// define cleanup logic that runs
// when component is unmounting
// or when or interval or immediate have changed
return () => {
cancelled = true;
clearInterval(id);
};
}, [interval, immediate]);
};
Then you can use the hook like this:
const [temperature, setTemperature] = useState();
useInterval(async (isCancelled) => {
try {
const response = await fetch('urlToWeatherData');
// check for cancellation after each await
// to prevent further action on a stale closure
if (isCancelled()) return;
if (response.status !== 200) {
// throw here to handle errors in catch block
throw new Error(response.statusText);
}
const [{ temperature }] = await response.json();
if (isCancelled()) return;
console.log(temperature);
setTemperature(temperature);
} catch (err) {
console.log('Fetch Error:', err);
}
}, 15000, true);
We can prevent the callback from calling setTemperature() if the component is unmounted by checking isCancelled(). For more general use-cases of useInterval() when the callback is dependent on stateful variables, you should prefer useReducer() or at least use the functional update form of useState().
Related
I have a function handleSubmit that handles registering in Firebase in a react component. Inside, I want to handle errors with my setErrorTimeout function, which has a setTimeout that resets the error automatically after 3 seconds in this case..
The problem is, my Timeout is not executed, e.g the callback function inside the timeout is not being executed after 3 seconds, but everything else is.. why?
const handleSubmit = async e => {
e.preventDefault()
console.log(formDetails)
if (formDetails.password !== formDetails.passwordrepeat) {
setErrorTimeout(setRegisterError, {
message: 'Passwords do not match!',
})
return
}
console.log('Try')
console.log(formDetails.email, formDetails.password)
try {
auth.createUserWithEmailAndPassword(
formDetails.email,
formDetails.password
)
.then(userCredentials => {
if (userCredentials) {
const user = userCredentials.user
let success = user.sendEmailVerification()
console.log('success register:', success)
setRegisterSuccess(
'You registered successfully! please check your email!'
)
setFormDetails({})
}
})
.catch(error => {
console.log('ERROR!')
setErrorTimeout(error)
})
} catch (e) {
setErrorTimeout(e)
}
}
const setErrorTimeout = error => {
console.log('inside timeout!')
setRegisterError(error)
const timer = setTimeout(() => {
console.log('inside cb!')
setRegisterError(null)
}, 3000)
clearTimeout(timer)
console.log('after timeout!')
}
You're clearing the timeout right after you create it here:
const timer = setTimeout(() => {
console.log('inside cb!')
setRegisterError(null)
}, 3000)
clearTimeout(timer)
You probably want that clearTimeout call to be inside the callback, although it's not even strictly needed since the timeout already fired.
I have a function that sends data to the server and uses props and set.... It is the same throughout few components. It gets called when a certain event occurs.
How can I refactor it out of those components into a single place?
I was thinking about using hooks but because it gets triggered by an event I don't think using a hook is a good approach.
async function sendDataToServer(data) {
const url = new URL(buildUrl());
let timeout = setTimeout(() => setPostingState(SendingState.Sending), 250);
try {
const response = props.id
? await axios.put(url, data)
: await axios.post(url, data);
setPostingState(SendingState.Idle);
props.onSaved(props.id ? props.id : response.data, data);
}
catch (error) {
setPostingState(SendingState.Error);
}
clearTimeout(timeout);
}
function handleSubmit(e) { ... sendDataToServer(data); ... }
You can make a curried function:
// helpers.js
export const genSendDataToServerCallback = ({ setState, onSaved, id }) => async (
data
) => {
const url = new URL(buildUrl());
let timeout = setTimeout(() => setState(SendingState.Sending), 250);
try {
const response = await (props.id
? axios.put(url, data)
: axios.post(url, data));
setState(SendingState.Idle);
onSaved(id ? id : response.data, data);
} catch (error) {
setState(SendingState.Error);
}
clearTimeout(timeout);
};
// Usage in some component
import { genSendDataToServerCallback } from './helpers.js'
const sendDataToServer = genSendDataToServerCallback({setter:setPostingState, ...props});
function handleSubmit(e) { sendDataToServer(data); }
// Usage in other component with different setter
const sendDataToServer = genSendDataToServerCallback({setter:setState, ...props});
function handleSubmit(e) { sendDataToServer(data); }
I have this code in my ReactJS web application:
useEffect(() => {
const fetchInfo = async () => {
const res = await fetch(`${api}&page=${page}`);
setLoading(true);
try {
const x = await res.json();
if (page === 1) {
setItems(x);
setAutoplay(true);
} else {
setItems({
hasMore: x.hasMore,
vacancies: [...items.vacancies, ...x.vacancies],
});
}
} catch (err){
console.log(err);
}
setLoading(false);
};
fetchInfo();
}, [page]);
When this component unmounts while running asynchronous function, it throws an error in console.
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.
How can i cancel asynchronous tasks in cleanup.
I'm assuming here that setLoading is the function setting the state after your component has un-mounted, and therefore throwing this warning. If yes, then what you need is a clean-up function.
The function passed to useEffect can return a function, which will be called before the component unmounts (you can think of it as the equivalent of the old componentWillUnmount) - more details here:
https://reactjs.org/docs/hooks-effect.html#example-using-hooks
Now what you probably want is some sort of flag to check whether it is safe for you to call setLoading, i.e, set that flag to be true by default, then set it to false in the return function. Here's a good article that should help:
https://juliangaramendy.dev/use-promise-subscription/
Now I haven't tested this but essentially your code would look something like this:
useEffect(() => {
const fetchInfo = async () => {
let isSubscribed = true;
const res = await fetch(`${api}&page=${page}`);
if (isSubscribed) setLoading(true);
try {
const x = await res.json();
if (page === 1) {
setItems(x);
setAutoplay(true);
} else {
setItems({
hasMore: x.hasMore,
vacancies: [...items.vacancies, ...x.vacancies]
});
}
} catch (err) {
console.log(err);
}
if (isSubscribed) setLoading(false);
return () => (isSubscribed = false);
};
fetchInfo();
}, [page]);
I have a function that refreshes the data of my component when the function is called. At this moment it only works for one component at a time. But I want to refresh two components at once. This is my refresh function:
fetchDataByName = name => {
const { retrievedData } = this.state;
const { fetcher } = this.props;
const fetch = _.find(fetcher, { name });
if (typeof fetch === "undefined") {
throw new Error(`Fetch with ${name} cannot be found in fetcher`);
}
this.fetchData(fetch, (error, data) => {
retrievedData[name] = data;
this._isMounted && this.setState({ retrievedData });
});
};
My function is called like this:
refresh("meetingTypes");
As it it passed as props to my component:
return (
<Component
{...retrievedData}
{...componentProps}
refresh={this.fetchDataByName}
/>
);
I tried passing multiple component names as an array like this:
const args = ['meetingTypes', 'exampleMeetingTypes'];
refresh(args);
And then check in my fetchDataByName function if name is an array and loop through the array to fetch the data. But then the function is still executed after each other instead of at the same time. So my question is:
What would be the best way to implement this that it seems like the
function is executed at once instead of first refreshing meetingTypes
and then exampleMeetingTypes?
Should I use async/await or are there better options?
The fetchData function:
fetchData = (fetch, callback) => {
const { componentProps } = this.props;
let { route, params = [] } = fetch;
let fetchData = true;
// if fetcher url contains params and the param can be found
// in the component props, they should be replaced.
_.each(params, param => {
if (componentProps[param]) {
route = route.replace(`:${param}`, componentProps[param]);
} else {
fetchData = false; // don't fetch data for this entry as the params are not given
}
});
if (fetchData) {
axios
.get(route)
.then(({ data }) => {
if (this.isMounted) {
callback(null, data);
}
})
.catch(error => {
if (error.response.status == 403) {
this._isMounted && this.setState({ errorCode: 403 });
setMessage({
text: "Unauthorized",
type: "error"
});
}
if (error.response.status == 401) {
this._isMounted && this.setState({ errorCode: 401 });
window.location.href = "/login";
}
if (error.response.status != 403) {
console.error("Your backend is failing.", error);
}
callback(error, null);
});
} else {
callback(null, null);
}
};
I assume fetchData works asynchronously (ajax or similar). To refresh two aspects of the data in parallel, simply make two calls instead of one:
refresh("meetingTypes");
refresh("exampleMeetingTypes");
The two ajax calls or whatever will run in parallel, each updating the component when it finishes. But: See the "Side Note" below, there's a problem with fetchDataByName.
If you want to avoid updating the component twice, you'll have to update fetchDataByName to either accept multiple names or to return a promise of the result (or similar) rather than updating the component directly, so the caller can do multiple calls and wait for both results before doing the update.
Side note: This aspect of fetchDataByName looks suspect:
fetchDataByName = name => {
const { retrievedData } = this.state; // <=============================
const { fetcher } = this.props;
const fetch = _.find(fetcher, { name });
if (typeof fetch === "undefined") {
throw new Error(`Fetch with ${name} cannot be found in fetcher`);
}
this.fetchData(fetch, (error, data) => {
retrievedData[name] = data; // <=============================
this._isMounted && this.setState({ retrievedData });
});
};
Two problems with that:
It updates an object stored in your state directly, which is something you must never do with React.
It replaces the entire retrievedData object with one that may well be stale.
Instead:
fetchDataByName = name => {
// *** No `retrievedData` here
const { fetcher } = this.props;
const fetch = _.find(fetcher, { name });
if (typeof fetch === "undefined") {
throw new Error(`Fetch with ${name} cannot be found in fetcher`);
}
this.fetchData(fetch, (error, data) => {
if (this._isMounted) { // ***
this.setState(({retrievedData}) => ( // ***
{ retrievedData: {...retrievedData, [name]: data} } // ***
); // ***
} // ***
});
};
That removes the in-place mutation of the object with spread, and uses an up-to-date version of retrievedData by using the callback version of setState.
I have a function that looks like following
export const checkForAvailableAgent = (topicId, serviceUrl, serviceId) => {
const serviceInfo = new window.adiaLive.ServiceInfo({
topicId: topicId, // set here the topicId which you want listen for
OnError: e => {
// react to error message (optional)
console.log("error: ", e);
},
OnServiceStateChange: e => {
if (e.ConnectedAdvisers > 0) {
// there are advisers online for given topicId
console.log("studio available");
return true;
} else {
console.log("studio not available");
return false;
}
}
});
serviceInfo.connect(serviceUrl, serviceId);
};
however the return statements don't return anything when I use the function in the following manner
useEffect(() => {
const agent = checkForAvailableAgent(
`sales_${i18n.language}`,
"https://linktoserviceurl",
"serviceid"
);
// console.log("studio available is: ", agent);
}, []);
the console.log massages appear but the return statement is undefined.
any help would be appreciated.
You can not return from a callback function, as it is running asynchronously and you are not waiting for it to have a result ready.
You can however make the function itself async by returning a Promise instead of the actual result and wait until the Promise has a result ready (e.g. it is resolved):
export const checkForAvailableAgent = (topicId, serviceUrl, serviceId) => {
return new Promise((resolve, reject) => {
const serviceInfo = new window.adiaLive.ServiceInfo({
topicId: topicId, // set here the topicId which you want listen for
OnError: e => {
// react to error message (optional)
console.log("error: ", e);
reject(); // reject on failure
},
OnServiceStateChange: e => {
if (e.ConnectedAdvisers > 0) {
// there are advisers online for given topicId
console.log("studio available");
resolve(true); // resolve instead of return
} else {
console.log("studio not available");
resolve(false);
}
}
});
serviceInfo.connect(serviceUrl, serviceId);
})
};
useEffect(() => {
checkForAvailableAgent(
`sales_${i18n.language}`,
"https://linktoserviceurl",
"serviceid"
).then((agent) => { // then callback is called when the promise resolved
console.log("studio available is: ", agent);
}).catch(error => { // catch is called when promise got rejected
console.log('An error happened');
});
}, []);
The function servceInfo.OnServiceStateChange is a function into the object (seems to be an event).
I'd suggest declaring a variable on the checkForAvailableAgent like connected and change it's value when the event is called.
Then access it using checkForAvailableAgent.connected.
A version with async/await and try/catch
export const checkForAvailableAgent = (topicId, serviceUrl, serviceId) => {
return new Promise((resolve, reject) => {
const serviceInfo = new window.adiaLive.ServiceInfo({
topicId: topicId,
OnError: reject,
OnServiceStateChange: e => resolve(e.ConnectedAdvisers > 0)
});
serviceInfo.connect(serviceUrl, serviceId);
})
};
useEffect(() => {
(async () => {
try {
const isAvailable = await checkForAvailableAgent(
`sales_${i18n.language}`,
"https://linktoserviceurl",
"serviceid"
);
// console.log("Result", isAvailable)
} catch(e) {
console.error(e)
}
})()
// console.log("studio available is: ", agent);
}, []);
There are 2 possible reasons
you are not returning anything from checkForAvailableAgent.
After returning from the checkForAvailableAgent, it might be asynchronous function. You can use async & await.