Correct way to catch fetch errors in React? - javascript

I have a simple react component that looks like this:
class Test extends React.Component {
componentDidMount() {
fetch('/some-url-here')
.then((data) => {
this.setState({ data });
})
.catch(() => {
alert('failed to fetch');
});
}
render() {
// render the data here
}
}
The problem with this is that the catch doesn't just catch fetch errors. It also catches any exceptions thrown in render ! What would be the correct way to create a simple component that fetches some data and handles fetch errors?

class Test extends React.Component {
componentDidMount() {
fetch('/some-url-here')
.then((data) => {
this.setState({ data });
}, (error) => {
if (error) {
// handle error here
}
});
}
render() {
// render the data here
}
}
If you use a catch instead of second callback, any errors that might've occurred during setState method would be left unhandled. So you can capture render method errors in your own way.
For more info, give a reading on this tweet by Dan Abramov.
Dan's Tweet

Related

Catching a warning with Error Boundary (React)

Noob here, not too sure about component configuration, so I ended up with an routing warning error (i.e. "No routes matched location "/play") which is displayed as a warning.
I'm tying to convert this into an error, and then hand it over to ErrorBoundary. It gets converted, but it's not being caught, so ErrorBoundary doesn't activate. What am I doing wrong?
//coverting the warning --No routes matched location "/play"-- into an error
console.warn = function (...args) {
setTimeout(() => {
throw new Error(args[0]);
}, 0);
};
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error("Error boundary caugh an error", error, info);
}
Convert a warning into an error and handle it with ErrorBoundary
According to react documents
Error boundaries do not catch errors for asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks).
Asynchronous errors can be handled using unhandledrejection event.
import { useEffect } from "react";
export default function ErrorBoundary({ children }) {
const captureReject = (e) => {
e.preventDefault();
console.log(error);
};
useEffect(() => {
window.addEventListener('unhandledrejection', captureReject);
return () => {
window.removeEventListener('unhandledrejection', captureReject);
}
}, []);
return children;
}
You can use it like this.

How can I set state in a function that is in a utility file?

I had a function in a react class that I was using to pull data.
It was working great, but I then found out I needed to use the same function in a few other places.
So I decided to put the function in it's own file so I could re-use it.
So here it is:
import axios from 'axios';
export const getGalaxyName = async (id) => {
try {
const { data: response } = await axios.get(`/api/scienceClass/galaxy/${id}`)
this.setState({ galaxyName: response.name });
}
catch (error) {
console.log(error);
}
}
And then in a component, I use it like this:
import { getGalaxyName } from './ScienceClassUtils';
render() {
getGalaxyName(slide.z_GalaxyId);
But now I am getting this error:
ScienceClassUtils.js:35 TypeError: Cannot read property 'setState' of
undefined
I am guessing it's because I'm trying to still set the state in the function like I did when it was originally inside the react class.
So how can I still use it now that it's separated out in another file, but still have this.setState({ galaxyName: response.name }); ?
Thanks!
Easiest way from here to there might be to have it return the promise instead of calling setState directly:
const { data } = await axios.get(`/api/scienceClass/galaxy/${id}`)
return data.name;
Then your component or anyone else could do whatever they need to do with it:
getGalaxyName(id).then(galaxyName => this.setState({ galaxyName });
There are at least two possible approaches:
Create a helper method for setting the galaxyName and then pass it to axios utility, and it will call it and pass the response.name to it.
Return the response from axios utility and use Promise methods .then, .catch, .finally, to handle the success, fail, finished cases as needed. Note that, the data you return from the axios utility will be passed to these methods as parameter.
The bottom line is you can't set the state outside a React component. Make your utility function return the data you need and set the state from within your react component.
Refactor your util function to look like this
import axios from 'axios';
export const getGalaxyName = async (id) => {
try {
const { data: response } = await axios.get(`/api/scienceClass/galaxy/${id}`)
return { galaxyName: response.name }
} catch (error) {
console.log(error);
}
}
From within your component call the method in a componentDidMount. lifecycle hook like this
class SampleComponent extends React.component {
// Add this lifecycle hook
componentDidMount() {
getGalaxyName(slide.z_GalaxyId).then((data) => {
this.setState(data)
});
}
render() {
// use state data as needed here
}
}

component's render() is not getting invoked after updating mobx store value

Updating store before fetching data from server, works fine.. But after fetching data from from server and updating store, render() method not getting invoked
code snippet
#action
static getPartner(partnerParams) {
store.invitationDetails.invitingOrgPartnerName = ""; // here render() is getting invoked
fetchPartner(partnerParams)
.then((data) => data.json())
.then(function (result) {
if (result.success) {
if (result.alreadyPartner) {
runInAction(() => {
store.invitationDetails.invitingOrgPartnerName = result.partnerName; // here render() is NOT getting invoked
});
}
}
})
.catch((e) => {
console.info("Failed getting partners", e);
});
}
Mobx works perfectly in most of the cases but not always
You should use extendObservable
Please follow this ExtendObservable

Error when using a function that requests API data

I'm working on this app that takes data from a movies API and I want to work with it.
I have this function that gets the API data:
/** #format */
const fetchMovie = movie => {
var APIKEY = "xxxxxxxxxxxxxxxxxxxxxx";
var API2 =
"https://api.themoviedb.org/3/search/movie?api_key=xxxxxxxxxx&language=en-US&page=1&include_adult=false&query=avengers";
var API = `https://api.themoviedb.org/3/search/movie?api_key=${APIKEY}&language=en-US&page=1&query=${movie}`;
fetch(API2)
.then(data => data.json())
.then(movies => console.log(movies) || movies.items)
.catch(error => {
console.log(error);
return null;
});
};
export default fetchMovie;
And I have this App class that uses the API data:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
activeMovie: "Avengers",
loading: true,
allMovies: []
};
}
componentDidMount() {
this.getData(this.activeMovie);
}
componentDidUpdate(prevState) {
if (prevState.activeMovie !== this.state.activeMovie) {
this.getData(this.state.activeMovie);
}
}
getData(movie) {
this.setState({
loading: true
});
fetchMovie(movie).then(data => {
this.setState({
allMovies: data,
loading: false
});
});
}
Now, before this I have used the same methodology and it worked but I don't know why the I get
TypeError: Object(...)(...) is undefined // this line fetchMovie(movie).then(data => {
The API is good, I can console log the data before it gets to the App component, but the function in the app component somehow doesn't work. any clues?
That's simply because your function fetchMovie() doesn't return a Promise so that you than use .then() after it. You can return a promise instead. However the logic in your code is probably a bit shaky. You might as well look that up because it goes into an infinite loop, consider debugging component life cycles for that.
To return a promise from your function, you can use a similar approach as I wrote in here: https://codesandbox.io/s/small-sun-sfcyv.
You are not returning any promise from your fetchMovie function, that way you can't use the .then so right now you only have access to that data in your fetchMovie. A possible solution would be defining your function as async and then you would be able to return your data from that function.
Try this.
/** #format */
const fetchMovie = movie => {
var APIKEY = "xxxxxxxxxxxxxxxxxxxxxx";
var API2 =
"https://api.themoviedb.org/3/search/movie?api_key=xxxxxxxxxx&language=en-US&page=1&include_adult=false&query=avengers";
var API = `https://api.themoviedb.org/3/search/movie?api_key=${APIKEY}&language=en-US&page=1&query=${movie}`;
return fetch(API2)
.then(data => data.json())
.then(movies => console.log(movies) || movies.items)
.catch(error => {
console.log(error);
return null;
});
};
export default fetchMovie;

Asynchronous ActionCreator in React Redux

I'm pretty new in React-Redux. Was working on an application. The thing is that I faced some issues with asynchronous execution of Redux actionCreator, may be.
Below is my component. Say, I want to call an actionCreator from componentDidMount() or from an onclick event listener.
class Dashboard extends PureComponent {
componentDidMount() {
this.props.getProductsAndPackages();
let something = [];
something = this.props.products;
}
....................................
}
Or , the function this.props.getProductsAndPackages(); can be an onClick event handler that does the same thing, context is the same. I'll ask my question after first explaining my code.
At the lower side of my Dashboard container:
Dashboard.propTypes = {
getProductsAndPackages: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
.......................
};
const mapStateToProps = (state) => {
return {
.....................
products: state.products.products,
...................
};
};
const mapDispatchToProps = (dispatch) => {
return {
getProductsAndPackages: () => dispatch(getProductsAndPackagesActionCreator()),
};
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Dashboard));
My actionCreator goes like:
export const getProductsAndPackagesActionCreator = () => {
return (dispatch) => {
dispatch(productsIsLoading(true));
let url = 'xyz';
if(!!localStorage.getItem('_token')) {
const local_token = localStorage.getItem('_token');
const fullToken = 'Bearer '.concat(local_token);
axios.get(url, {headers: {Authorization: fullToken}})
.then(response => {
dispatch(productsIsLoading(false));
if (response.data.statusCode === 200) {
dispatch(productsFetched(true));
dispatch(products(response.data.data));
} else {
dispatch(productsFetched(false));
dispatch(productsErrors(response.data.message));
}
})
.catch(error => {
});
} else {
axios.get(url)
.then(response => {
dispatch(productsIsLoading(false));
if (response.data.statusCode === 200) {
dispatch(productsFetched(true));
dispatch(products(response.data.data));
} else {
dispatch(productsFetched(false));
dispatch(productsErrors(response.data.message));
}
})
.catch(error => {
console.log(error);
dispatch(productsIsLoading(false));
dispatch(productsErrors(error.message));
});
}
};
};
Now, I want my getProductsAndPackagesActionCreator() to return a Promise or anything that would allow my something variable to get the actual data returned from the server. Right now, by the time I'm getting actual data, the line something=this.props.products has already been executed and I get back the initialValue that was set for products.
I know, whenever I'll receive the populated products, component will re-render, but that does not help my decision making.
I'm using redux-thunk, by the way.
What should I do now ? Sorry for such a long post.
Actually I wanted getProductsAndPackagesActionCreator() to return a promise, which was pretty straightforward, to be honest. I figured out that if you just return the axios.get() or axios.post(), it will return a promise. So, the modified code looked like below:
export const getProductsAndPackagesActionCreator = () => {
return (dispatch) => {
dispatch(productsIsLoading(true));
let url = 'xyz';
if(!!localStorage.getItem('_token')) {
return axios.get(url, {headers: {Authorization: fullToken}})
.then(response => {
............
............
})
.catch(error => {
});
} else {
return axios.get(url)
.then(response => {
...........
...........
})
.catch(error => {
console.log(error);
});
}
};
};
And then, I could do something like below in componentDidMount() or on any onClick event:
this.props.getProductsAndPackages().then(() => {
this.setState({
...this.state,
clicked_product: this.props.product_by_id
}, () => {
//do other stuffs
});
});
Feel free to let me know if there's any issue.
I think you are close to getting what you want. First of all, you should understand that redux actions and react actions like setState are asynchronous, so you have to apply your logic keeping this in mind. I'm going to explain what i think in some points:
You have called the action creator in the correct place componentDidMount, also you can call this action in any onClick if you want.
As soon as you dispatch the action you are changing your redux state setting loading true I suppose. So now you can access this property in your render function, so you can render a Loader until your api call finishes.
When your ajax function finishes, with an error or not, I suppose you are setting loading to false and updating your products data, so you can render now your loaded products in your dashboard.
Are you sure that you have to compare your empty products array with the received data? Maybe you can check in your render function if (!this.props.products.length) return null, when you load your page you will see a loader function and later your dashboard with the products.
If you really need to compare previous products with received products componentDidUpdate is your method. In this method, you can access your previous props and compare with actual props, be careful comparing arrays, remember [] === [] is false. Maybe you can compare the length, something like
componentDidUpdate(prevProps){
if(prevProps.products.length !=== this.props.products.lenth){
doSomething()
}
}
Just to say that componentDidUpdate is executed after render, so be careful with your code to no-execute extra renderings.
Hope it helps, if you dont understand anyting just tell me :)

Categories

Resources