I have a React app that can makes a series of fetch requests, depending on user interactions. I want to abort old fetch requests any time the app receives a new one.
To accomplish this, I've created a custom hook, useData. Its main fucntion is running a useEffect hook whenever the url changes. The easiest way to abort old requests seems to me to be using the cleanup mechanism provided by useEffect. But this will call abort on all requests, not just incomplete ones.
Are there any hidden problems this might cause? It seems that it shouldn't do much to a resolved fetch, but I can't find any documentation to support that.
This is my code:
/** Returns data for a given url. */
export const useData = function (dataUrl) {
const [loadedData, setLoadedData] = useState(Object.assign({}));
// runs if/when the url changes
useEffect(() => {
// new controller for each new url
const controller = new AbortController();
async function getData(dataUrl) {
if (!dataUrl) return;
// load data & set state
try {
const data = await fetch(dataUrl, {
signal: controller.signal,
});
setLoadedData(data);
} catch (e) {
console.error(`useData failed for url ${dataUrl}\n${e}.`);
}
}
getData(dataUrl);
// clean up the last request before running
// useEffect for on the next url
return () => {
controller.abort();
};
}, [dataUrl]);
// return data in the hook's state, set by useEffect
return loadedData;
};
Fetch returns a promise so, once it resolves, any abortion does not have any effects.
You can run this simple demo in the console, you'll see that the request completes (at least until connection) and no errors are generated:
async function test() {
const controller = new AbortController()
const request = await fetch('https://stackoverflow.com', {signal: controller.signal})
console.log('The request was ok?', request.ok)
controller.abort();
}
test();
Related
Whenever if there is any asynchronous task performing related to component and that component unmounts then React generally gives this 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.
I found some solutions over internet to use isMount flag (either by using it with useRef or useState) as true and then updating it to false on component unmount. But is that a proper solution as per React site using isMount is a antipattern.
https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
In future version of React, you probably won't need to fix this. As React dev team is going to remove this warning in future release. The main reason being this warning can be false positive sometimes.
As per this commit by Dan Abramov
https://github.com/facebook/react/pull/22114
But what are the solutions to fix this till that version release -
Using isMountState anti-pattern - If someone is checking isMounted in his code to fix this issue, then that person is already too late in performing this check, as this warning indicates the same check is done by React and it failed.
If this issue is because of an asynchronous call. Then one possible solution is to use AbortController API in your code.
AbortController API helps in aborting any ajax call that is already made. Cool stuff. Right?
More details on this can be found here
Abort Controller1
So if it is a fetch API one can use AbortController API like this
useEffect(() => {
const abortController = new AbortController()
// creating an AbortController
fetch(url, {
signal: abortController.signal
})
// passing the signal to the query
.then(data => {
setState(data)
// if everything went well, set the state
})
.catch(error => {
if (error.name === 'AbortError') return
// if the query has been aborted, do nothing
throw error
})
return () => {
abortController.abort()
// stop the query by aborting on the AbortController on unmount
}
}, [])
If you are using axios, then good news is axios also provides support for AbortController APIs -
const fetchData = async (params) => {
setLoading(true);
try {
const result = await axios.request(params);
// more code here
} catch (curError) {
if (axios.isCancel(curError)) {
return false;
}
// error handling code
}
return null;
};
useEffect(() => {
const cancelToken = axios.CancelToken;
const source = cancelToken.source();
fetchData({
...axiosParams,
cancelToken: source.token
});
return () => {
source.cancel("axios request cancelled");
};
}, []);
I am getting the error below. I assume this is because I'm still subscribed to the Firebase database even when my component unmounts. I am trying to take advantage of the real-time features so that whenever an item is deleted from the list it will reflect on the UI.
I have created multiple functions with a single purpose to fetch different documents. Below is one example.
export const getAllTask = (userId) => {
return new Promise((resolve, reject) => {
const db = database.ref('tasks');
db.child(`${userId}/taskItems`).on('value', (snapshot) => {
const user = snapshot.val();
if (user) {
resolve(user);
} else {
reject(user);
}
});
});
};
Then whenever any of my components, it runs my useEffect to fetch data however when it unmounts how can I correctly use off() or clean up correctly? Is there a better approach to do this?
useEffect(() => {
const test = async (userId) => {
await getAllTask(userId).then((result) => {
setItems(result);
});
};
test(userId);
}, [userId]);
try this ,useEffect returns a function that is called when component unmounts
useEffect(() => {
let mounted=true;
const test = async (userId) => {
await getAllTask(userId).then((result) => {
if(mounted) setItems(result)
});
};
test(userId);
return ()=>{mounted=false}
}, [userId]);
In this case, you shouldn't use on() at all. Use once() for reading data a single time. It returns a promise that's easy to work with. on() is for persistent listeners that deliver updates over time every time something changes with the results of the query.
Since promises can only resolve a single time, it doesn't make sense to wrap a persistent listener in a promise.
If you do actually want listener updates over time, for as long as the component is mounted, don't wrap it in a promise. instead, you should instruct your useEffect to unsubscribe the listener by returning a function that it can invoke to shut things down.
See also: How to properly update state with Firebase and useEffect()? In the answer there, see how the useEffect hook returns a function that calls off().
I recently started migrating things from jQ to a more structured framework being VueJS, and I love it!
Conceptually, Vuex has been a bit of a paradigm shift for me, but I'm confident I know what its all about now, and totally get it! But there exist a few little grey areas, mostly from an implementation standpoint.
This one I feel is good by design, but don't know if it contradicts the Vuex cycle of uni-directional data flow.
Basically, is it considered good practice to return a promise(-like) object from an action? I treat these as async wrappers, with states of failure and the like, so seems like a good fit to return a promise. Contrarily mutators just change things, and are the pure structures within a store/module.
actions in Vuex are asynchronous. The only way to let the calling function (initiator of action) to know that an action is complete - is by returning a Promise and resolving it later.
Here is an example: myAction returns a Promise, makes a http call and resolves or rejects the Promise later - all asynchronously
actions: {
myAction(context, data) {
return new Promise((resolve, reject) => {
// Do something here... lets say, a http call using vue-resource
this.$http("/api/something").then(response => {
// http success, call the mutator and change something in state
resolve(response); // Let the calling function know that http is done. You may send some data back
}, error => {
// http failed, let the calling function know that action did not work out
reject(error);
})
})
}
}
Now, when your Vue component initiates myAction, it will get this Promise object and can know whether it succeeded or not. Here is some sample code for the Vue component:
export default {
mounted: function() {
// This component just got created. Lets fetch some data here using an action
this.$store.dispatch("myAction").then(response => {
console.log("Got some data, now lets show something in this component")
}, error => {
console.error("Got nothing from server. Prompt user to check internet connection and try again")
})
}
}
As you can see above, it is highly beneficial for actions to return a Promise. Otherwise there is no way for the action initiator to know what is happening and when things are stable enough to show something on the user interface.
And a last note regarding mutators - as you rightly pointed out, they are synchronous. They change stuff in the state, and are usually called from actions. There is no need to mix Promises with mutators, as the actions handle that part.
Edit: My views on the Vuex cycle of uni-directional data flow:
If you access data like this.$store.state["your data key"] in your components, then the data flow is uni-directional.
The promise from action is only to let the component know that action is complete.
The component may either take data from promise resolve function in the above example (not uni-directional, therefore not recommended), or directly from $store.state["your data key"] which is unidirectional and follows the vuex data lifecycle.
The above paragraph assumes your mutator uses Vue.set(state, "your data key", http_data), once the http call is completed in your action.
Just for an information on a closed topic:
you don’t have to create a promise, axios returns one itself:
Ref: https://forum.vuejs.org/t/how-to-resolve-a-promise-object-in-a-vuex-action-and-redirect-to-another-route/18254/4
Example:
export const loginForm = ({ commit }, data) => {
return axios
.post('http://localhost:8000/api/login', data)
.then((response) => {
commit('logUserIn', response.data);
})
.catch((error) => {
commit('unAuthorisedUser', { error:error.response.data });
})
}
Another example:
addEmployee({ commit, state }) {
return insertEmployee(state.employee)
.then(result => {
commit('setEmployee', result.data);
return result.data; // resolve
})
.catch(err => {
throw err.response.data; // reject
})
}
Another example with async-await
async getUser({ commit }) {
try {
const currentUser = await axios.get('/user/current')
commit('setUser', currentUser)
return currentUser
} catch (err) {
commit('setUser', null)
throw 'Unable to fetch current user'
}
},
Actions
ADD_PRODUCT : (context,product) => {
return Axios.post(uri, product).then((response) => {
if (response.status === 'success') {
context.commit('SET_PRODUCT',response.data.data)
}
return response.data
});
});
Component
this.$store.dispatch('ADD_PRODUCT',data).then((res) => {
if (res.status === 'success') {
// write your success actions here....
} else {
// write your error actions here...
}
})
TL:DR; return promises from you actions only when necessary, but DRY chaining the same actions.
For a long time I also though that returning actions contradicts the Vuex cycle of uni-directional data flow.
But, there are EDGE CASES where returning a promise from your actions might be "necessary".
Imagine a situation where an action can be triggered from 2 different components, and each handles the failure case differently.
In that case, one would need to pass the caller component as a parameter to set different flags in the store.
Dumb example
Page where the user can edit the username in navbar and in /profile page (which contains the navbar). Both trigger an action "change username", which is asynchronous.
If the promise fails, the page should only display an error in the component the user was trying to change the username from.
Of course it is a dumb example, but I don't see a way to solve this issue without duplicating code and making the same call in 2 different actions.
actions.js
const axios = require('axios');
const types = require('./types');
export const actions = {
GET_CONTENT({commit}){
axios.get(`${URL}`)
.then(doc =>{
const content = doc.data;
commit(types.SET_CONTENT , content);
setTimeout(() =>{
commit(types.IS_LOADING , false);
} , 1000);
}).catch(err =>{
console.log(err);
});
},
}
home.vue
<script>
import {value , onCreated} from "vue-function-api";
import {useState, useStore} from "#u3u/vue-hooks";
export default {
name: 'home',
setup(){
const store = useStore();
const state = {
...useState(["content" , "isLoading"])
};
onCreated(() =>{
store.value.dispatch("GET_CONTENT" );
});
return{
...state,
}
}
};
</script>
I'm using React Starter Kit to create my first React app, and I'm struggling with Ajax calls. I saw that this Kit embeds a way to perform Ajax calls (which is by the way used internally for app routing):
import fetch from '../../core/fetch';
I've added this in my component, and then try to perform an Ajax call when the component loads. Here is my code:
componentDidMount() {
var moduleManager = 'https://my_domain.com/my_endpoint';
async function getModules (url) {
const response = await fetch(url);
const content = await response.json();
return content;
};
this.state.modulesList = getModules(moduleManager);
console.log(this.state.modulesList);
}
I'm also using the state value in my render function:
render() {
var rows = [];
for (var i = 0; i < this.state.modulesList.length; i++) {
rows.push(
<li>{this.state.modulesList[i]}<li/>
);
}
This code put together logs this in my console:
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Then the Ajax call is performed (successfully) and my console is now showing this:
Promise
__proto__:Promise
[[PromiseStatus]]:"resolved"
[[PromiseValue]]:Array[16]
The desired behaviour is of course to update my view: when the ajax calls is performed, display one line per array member.
What am I missing?
Thanks
What I suggest doing:
constructor() {
...
// Bind your async function to your stateful component's class
this.getModules = this.getModules.bind(this);
}
async getModules(url) {
try {
// Perform the ajax call
const response = await fetch(url);
// Convert respone to json
const content = await response.json();
// Process your content (if needed)
...
// Call setState() here
this.setState({someContent: content});
} catch(e) {
console.error(e);
}
}
componentDidMount() {
this.getModules(`${URL}`);
}
You can't actually return the fetched/parsed data from an async function. According to this MDN link, async function returns a promise, and not the actual data you'd expect to get after parsing it.
What happened in your case, was that you were actually trying to receive a returned value from an async function, inside a regular(sync) function (componentDidMount). You can either do what I suggested, or use .then() to use setState after resolving and parsing the promise, in the actual componentDidMount function.
I suggest reading about async functions and Promise before continuing.
Best of luck!
Without testing your code, one problem is that you're incorrectly modifying state directly. That doesn't trigger a render and therefore your view is not updated. Try setState() instead, like so:
<li>{this.setState({modulesList[i]})}<li/>
I'm working on an app in Electron, React and Redux. At the program start, I'm making a few asynchronous ipc calls between the render process and the main process and save the results in the store.
// in the main app component
componentWillMount() {
const { store } = this.context;
store.dispatch(fetchOptions());
store.dispatch(fetchRequirements());
store.dispatch(fetchStats());
/* miracle function */
},
// actions.js
export function fetchOptions() {
return dispatch => {
dispatch(requestOptions());
ipcRenderer.on('sendOptions', function(event, arg) {
dispatch(receiveOptions(arg));
});
ipcRenderer.send('requestOptions', '');
};
}
// others accordingly
receiveOptions(arg), receiveRequirements(arg) and receiveStats(arg) are action creators and finally the reducer will save the responses in the store.
Directly after store.dispatch(fetchStats()), I want to dispatch another action to make some calculations based on the values that were loaded into the store. However, this action will usually be dispatched before the responses from ipc arrive.
I found this discussion with a similar problem, but they are making api calls with fetch instead of ipc messages, and I wouldn't know how to apply their idea to my problem.
So here is my question: how can I make the program wait for responses from all the channels before continuing?
Edit: When I set a time out of length 0 for the dispatches after the ipc calls, it works at least with immediate responses, but of course it doesn't help when the responses take a bit longer.
store.dispatch(fetchOptions());
store.dispatch(fetchRequirements());
store.dispatch(fetchStats());
setTimeout(function() {
store.dispatch(calculateThis());
store.dispatch(calculateThat());
}, 0);
An example using Promises
Assumption
I'm not familiar with how your icpRenderer works, or exaclty when the dispatching is completed. I am going to assume that the dispatch is completed after the call dispatch(receiveOptions(arg)) returns in
ipcRenderer.on('sendOptions', function(event, arg) {
dispatch(receiveOptions(arg));
});
If dispatch() is asynchronous, this will not work (unless you wait to resolve the promise until after dispatch() is done).
If my assumption is correct
you should be able to return receive a "promise" (and resolve it) like this
// actions.js
export function fetchOptions(promise) {
return dispatch => {
dispatch(requestOptions());
ipcRenderer.on('sendOptions', function(event, arg) {
dispatch(receiveOptions(arg));
if (promise) promise.resolve(); // Resolve the promise
});
ipcRenderer.send('requestOptions', '');
}
}
// return Promises others accordingly
(Note that you may call fetchOptions without passing a "promise", because we only call promise.resolve() if promise is present. Hence, this should not complicate your existing code.)
In order to wait for the promises to resolve, you can do like this
// in the main app component
componentWillMount() {
const { store } = this.context;
const promises = [
new Promise((resolve, reject) =>
store.dispatch(fetchOptions({resolve, reject}))),
new Promise((resolve, reject) =>
store.dispatch(fetchRequirements({resolve, reject}))),
new Promise((resolve, reject) =>
store.dispatch(fetchStats({resolve, reject})))
];
Promise.all(promises).then(() =>
// Dispatch another action after the first three dispatches are completed.
);
},
The code didn't turn out super clean, but hopefully it will at least work.