Aborted fetch is still calling "then" function? - reactjs

I have a react component which calling fetch/abort on mount/unmount,
when I tried to move the fetch function to a separated class the "then" function is keep calling even after aborting the fetch request so the "Not Aborted" text is printed to the console.
It looks like this:
ReactComponent
class ReactComponent extends Component {
constructor(props) {
super(props);
this.TodoService = new TodoService();
}
componentDidMount() {
this.TodoService.fetch().then(res => {
console.log("Not Aborted");
});
}
componentWillUnmount() {
this.TodoService.abort();
}
}
TodoService
class TodoService {
constructor() {
this.controller = new AbortController();
this.signal = this.controller.signal;
}
handleErrors(response) {
if (response.ok) {
return response;
}
throw Error(response.statusText);
}
fetch() {
return fetch(`todos`, {
signal: this.signal
})
.then(this.handleErrors)
.then(res => res.json())
.catch(err => console.error(err));
}
abort() {
this.controller.abort();
}
}

I have a feeling it could come down to the this context.
try adding these to the TodoService constructor:
this.fetch = this.fetch.bind(this);
this.abort = this.abort.bind(this);
You are calling those methods from a different context and the current contexts this will be used.
Here is the browser support for the AbortController

You can also try this:
fetch() {
return fetch(`todos`,
{signal: this.signal},
this.controller.abort()
)
.then(this.handleErrors)
.then(res => res.json())
.catch(err => console.error(err));
}

Related

Unable to access Api call data. Returns undefined. React

I am trying to make a movie search app with React and have made an API call to The Movie Database API. What I am trying to do is get the data of the new movie releases, but then make another API call to get the specific details for each of those new releases since that data is stored in a different location.
I am able to access the data from the first API call, but when I try to access the movie taglines from the second data object, the console outputs "Cannot read property 'tagline' of undefined".
App.js
class App extends Component {
constructor(props) {
super(props)
this.state = {
movieRows: [],
ids: [],
movieDetails: [],
}
this.performSearch = this.performSearch.bind(this);
}
componentDidMount() {
this.performSearch();
}
performSearch() {
const urlString = "https://api.themoviedb.org/3/movie/popular?api_key=6db3cd67e35336927891a72c05&language=en-US&page=1";
axios.get(urlString)
.then(res => {
const results = res.data.results
let movieRows = [];
let movieDetails = [];
results.forEach((movie) => {
movieRows.push(movie);
axios.get(`https://api.themoviedb.org/3/movie/${movie.id}?api_key=6db3cd67e35336927891a72c05&language=en-US`)
.then(res => {
movieDetails.push(res.data);
})
.catch(function (error) {
console.log(error);
});
});
this.setState({
movieRows: movieRows,
movieDetails: movieDetails,
});
})
.catch(function (error) {
console.log(error);
});
}
Content.js
export default class Content extends Component {
constructor(props) {
super(props)
this.state = {
name: 'Jonathan',
}
this.filmLoop = this.filmLoop.bind(this);
}
filmLoop() {
let movieData = this.props.globalState.movieRows;
let movieDetails = this.props.globalState.movieDetails;
return movieData.map((movie, index) => {
return (
<div className="film" key={index}>
<img className="poster" src={`http://image.tmdb.org/t/p/w342${movie.poster_path}`} alt="The Dark Knight poster" />
<div className="film-info">
<div className="film-title">
<h3>{movie.title}</h3>
</div>
<h4>{movieDetails[index].tagline}</h4>
*I get the error from the last line
Well the main issue is that you are calling setState outside your .then you have to update the state inside your then or your catch. This is because the promise is an async function, so you have to change the state only when the promise has been resolved of rejected.
performSearch() {
const urlString = "https://api.themoviedb.org/3/movie/popular?api_key=6db3cd67e35336927891a72c05&language=en-US&page=1";
axios.get(urlString)
.then(responsePopular => {
const results = responsePopular.data.results
let movieRows = [];
let movieDetails = [];
results.forEach((movie) => {
movieRows = [...movieRows, movie];
axios.get(`https://api.themoviedb.org/3/movie/${movie.id}?api_key=6db3cd67e35336927891a72c05&language=en-US`)
.then(responseMovie => {
movieDetails = [...movieDetails, responseMovie.data];
this.setState({
movieRows: movieRows,
movieDetails: movieDetails,
})
})
.catch(function (error) {
console.log(error);
});
});
})
.catch(function (error) {
console.log(error);
});
}
I think that this could solve your issue.

React doesn't render data coming from an api response

I've seen a lot of questions and I couldn't get the solution
here is my code:
import React, { Component } from "react";
import axios from "axios";
import "./tree.css";
import "./mainTree";
class TablesTree extends Component {
constructor(props) {
super(props);
this.data = this.props.info;
this.state = {
fields: [],
data: [],
show: false
};
}
componentDidMount() {
var dataGet = [];
this.props.tables.forEach((name, i) => {
this.getFieldsTable(name.TABLE_NAME, (err, res) => {
if (res) {
dataGet.push({
TABLE_NAME: name.TABLE_NAME,
columns: res
});
}
});
});
this.setState({ data: dataGet });
}
getFieldsTable(table, callback) {
axios
.get(`table/columns?name=${this.data.user}&psw=${this.data.password}&schema=${this.data.schema}&table=${table}`)
.then(response => {
callback(null, response.data);
})
.catch(error => {
console.log(error);
});
}
render() {
return (
<div>
{this.state.data
? this.state.data.map((itm, i) => {
return (
<div>
<h1>{itm.TABLE_NAME}</h1>
</div>
);
})
: null}
</div>
);
}
}
export default TablesTree;
I've made console.log of the this.state.data
and the data is in there, but it doesn't renders anything
I've tried a lot of soutions, but I still without rendering the data, I will apreciate your help.
There's a few things I would change about your code, but most importantly you need to do this.setState after your push to dataGet (inside of your callback function).
Because your API call is asynchronous, you are only calling setState once when your component is initially mounted (and while dataGet is still empty).
getFieldsTable is asynchronous, so the dataGet array will be empty when you call setState.
You could return the promise from getFieldsTable and use Promise.all on all the promises, and use the data when all of them have resolved.
Example
class TablesTree extends Component {
// ...
componentDidMount() {
const promises = this.props.tables.map(name => {
return this.getFieldsTable(name.TABLE_NAME).then(res => {
return {
TABLE_NAME: name.TABLE_NAME,
columns: res
};
});
});
Promise.all(promises).then(data => {
this.setState({ data });
});
}
getFieldsTable(table) {
return axios
.get(`table/columns?name=${this.data.user}&psw=${this.data.password}&schema=${this.data.schema}&table=${table}`)
.then(response => {
return response.data;
})
.catch(error => {
console.log(error);
});
}
// ...
}

AsyncStorage.getItem in react native not working as expected

I am trying to fetch data using AsyncStorage. whenever i call my action creator requestData and do console on the data which is passed , i get something like below .I have two version of getItem .In both the version i get useless value for property field . Property value should be readable
{"fromDate":"20160601","toDate":"20160701","property":{"_40":0,"_65":0,"_55":null,"_72":null},"url":"/abc/abc/xyz"}
async getItem(item) {
let response = await AsyncStorage.getItem(item);
let responseJson = await JSON.stringify(response);
return responseJson;
}
async getItem(item) {
try {
const value = AsyncStorage.getItem(item).then((value) => { console.log("inside componentWillMount method call and value is "+value);
this.setState({'assetIdList': value});
}).then(res => {
return res;
});
console.log("----------------------------value--------------------------------------"+value);
return value;
} catch (error) {
// Handle errors here
console.log("error is "+error);
}
}
componentWillMount() {
requestData({
fromDate: '20160601',
toDate: '20160701',
assetId: this.getItem(cmn.settings.property),
url: '/abc/abc/xyz'
});
}
You are getting property as a promise, you need to resolve it.
Try to use something link that.
assetId: this.getItem(cmn.settings.property).then((res) => res)
.catch((error) => null);
Since AsyncStorage is asynchronous in nature you'll have to wait for it to return the object AND THEN call your requestData method; something like the following -
class MyComponent extends React.Component {
componentWillMount() {
this.retrieveFromStorageAndRequestData();
}
async getItem(item) {
let response = await AsyncStorage.getItem(item);
// don't need await here since JSON.stringify is synchronous
let responseJson = JSON.stringify(response);
return responseJson;
}
async retrieveFromStorageAndRequestData = () => {
let assetId = await getItem(cmn.settings.property);
requestData({
fromDate: '20160601',
toDate: '20160701',
assetId,
url: '/abc/abc/xyz'
}) ;
}
// rest of the component
render() {
// render logic
}
}

Handling Axios error in React

I have a React component that calls a function getAllPeople:
componentDidMount() {
getAllPeople().then(response => {
this.setState(() => ({ people: response.data }));
});
}
getAllPeople is in my api module:
export function getAllPeople() {
return axios
.get("/api/getAllPeople")
.then(response => {
return response.data;
})
.catch(error => {
return error;
});
}
I think this is a very basic question, but assuming I want to handle the error in my root component (in my componentDidMount method), not in the api function, how does this root component know whether or not I the axios call returns an error? I.e. what is the best way to handle errors coming from an axios promise?
Better way to handle API error with Promise catch method*.
axios.get(people)
.then((response) => {
// Success
})
.catch((error) => {
// Error
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
// console.log(error.response.data);
// console.log(error.response.status);
// console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the
// browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
console.log(error.config);
});
The getAllPeople function already returns the data or error message from your axios call. So, in componentDidMount, you need to check the return value of your call to getAllPeople to decide whether it was an error or valid data that was returned.
componentDidMount() {
getAllPeople().then(response => {
if(response!=error) //error is the error object you can get from the axios call
this.setState(() => ({ people: response}));
else { // your error handling goes here
}
});
}
If you want to return a promise from your api, you should not resolve the promise returned by your axios call in the api. Instead you can do the following:
export function getAllPeople() {
return axios.get("/api/getAllPeople");
}
Then you can resolve in componentDidMount.
componentDidMount() {
getAllPeople()
.then(response => {
this.setState(() => ({ people: response.data}));
})
.catch(error => {
// your error handling goes here
}
}
My suggestion is to use a cutting-edge feature of React. Error Boundaries
This is an example of using this feature by Dan Abramov.
In this case, you can wrap your component with this Error Boundary component.
What is special for catching the error in axios is that you can use
interceptors for catching API errors.
Your Error Boundary component might look like
import React, { Component } from 'react';
const errorHandler = (WrappedComponent, axios) => {
return class EH extends Component {
state = {
error: null
};
componentDidMount() {
// Set axios interceptors
this.requestInterceptor = axios.interceptors.request.use(req => {
this.setState({ error: null });
return req;
});
this.responseInterceptor = axios.interceptors.response.use(
res => res,
error => {
alert('Error happened');
this.setState({ error });
}
);
}
componentWillUnmount() {
// Remove handlers, so Garbage Collector will get rid of if WrappedComponent will be removed
axios.interceptors.request.eject(this.requestInterceptor);
axios.interceptors.response.eject(this.responseInterceptor);
}
render() {
let renderSection = this.state.error ? <div>Error</div> : <WrappedComponent {...this.props} />
return renderSection;
}
};
};
export default errorHandler;
Then, you can wrap your root component passing axios instance with it
errorHandler(Checkout, api)
As a result, you don't need to think about error inside your component at all.
You could check the response before setting it to state. Something like
componentDidMount() {
getAllPeople().then(response => {
// check if its actual response or error
if(error) this.setState(() => ({ error: response }));
else this.setState(() => ({ people: response}));
});
}
Its relying on the fact that axios will return different objects for success and failures.
The solution from Yevhenii Herasymchuk was very close to what I needed however, I aimed for an implementation with functional components so that I could use Hooks and Redux.
First I created a wrapper:
export const http = Axios.create({
baseURL: "/api",
timeout: 30000,
});
function ErrorHandler(props) {
useEffect(() => {
//Request interceptor
http.interceptors.request.use(function (request) {
// Do something here with Hooks or something else
return request;
});
//Response interceptor
http.interceptors.response.use(function (response) {
if (response.status === 400) {
// Do something here with Hooks or something else
return response;
}
return response;
});
}, []);
return props.children;
}
export default ErrorHandler;
Then I wrapped the part of the project that I needed to check how axios behaved.
<ErrorHandler>
<MainPage/>
</ErrorHandler>
Lastly, I import the axios instance(http) wherever I need it in the project.
Hope it helps anyone that wishes for a different approach.
I just combined both answers by Yevhenii Herasymchuk and GeorgeCodeHub, fixed some mistakes and ported it into React hooks. So here is my final working version:
// [project-directory]/src/lib/axios.js
import Axios from 'axios';
const axios = Axios.create({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
withCredentials: true,
});
export default axios;
// [project-directory]/src/components/AxiosErrorHandler.js
import {useEffect} from 'react';
import axios from '#/lib/axios';
const AxiosErrorHandler = ({children}) => {
useEffect(() => {
// Request interceptor
const requestInterceptor = axios.interceptors.request.use((request) => {
// Do something here with request if you need to
return request;
});
// Response interceptor
const responseInterceptor = axios.interceptors.response.use((response) => {
// Handle response here
return response;
}, (error) => {
// Handle errors here
if (error.response?.status) {
switch (error.response.status) {
case 401:
// Handle Unauthenticated here
break;
case 403:
// Handle Unauthorized here
break;
// ... And so on
}
}
return error;
});
return () => {
// Remove handlers here
axios.interceptors.request.eject(requestInterceptor);
axios.interceptors.response.eject(responseInterceptor);
};
}, []);
return children;
};
export default AxiosErrorHandler;
Usage:
// Wrap it around your Layout or any component that you want
return (
<AxiosErrorHandler>
<div>Hello from my layout</div>
</AxiosErrorHandler>
);

React lifecycle methods: fetch in componentDidMount

I'm trying to do a simple fetch through the componentDidMount lifecycle method. However, the result does not appear on the page as it should unless I have a one second timeout. I've gathered it's due to the async nature of the fetch, but how can I fix that without having to use setTimeout? Would componentDidUpdate work/how would you use it?
constructor(props) {
super(props);
this.state = { value: '' };
this.getValue= this.getValue.bind(this);
}
getValue() {
return (
fetch(url, {
method: 'GET',
}).then(response => {
if (response.status >= 400) {
throw new Error('no response: throw');
}
return response.json()
}).then(response => {
this.setState({value: response});
}).catch((error) => {
this.setState({
value: 'no response: catch'
})
})
);
}
componentDidMount(){
//this.getValue(); //does not work
setTimeout(() => this.getValue(), 1000); //this works & populates page
}
render() {
return (
<div>
<div>{this.state.value}</div>
</div>
)
}
Be sure you are binding your this.getValue method to the proper context in the constructor. When you put it in your setTimeout, you have it in a fat arrow function which binds to this implicitly.
constructor(props) {
super(props);
this.getValue = this.getValue.bind(this);
}
getValue() { ... }
componentDidMount() {
this.getValue();
}

Resources