I'm building a small app that consumes a REST api. I'm running into problems displaying information inside arrays of objects, see code below:
actions.js
import axios from 'axios'
function fetchService () {
return axios.get('http://localhost:5000/ldbws-rest-proxy/v0.1/departure-board/IPS')
.then(function (response) {
return {
service: response.data.trainServices[0]
}
})
.catch(function (response) {
console.log(response)
})
}
export default fetchService
train_service.js
import fetchService from '../actions'
import DepartureTime from './departure_time'
import OriginStation from './origin_station'
var TrainService = React.createClass ({
getInitialState () {
return {
service: []
}
},
componentDidMount () {
fetchService()
.then(function (dataObj) {
this.setState({
service: dataObj.service
})
}.bind(this))
},
render () {
return (
<section>
<DepartureTime time={this.state.service.std} />
<OriginStation name={this.state.service.origin[0].crs} />
</section>
)
}
})
export default TrainService
JSON sample (response.data.trainServices[0])
{
"destination": [
{
"crs": "CBG",
"locationName": "Cambridge"
}
],
"etd": "On time",
"operator": "Greater Anglia",
"operatorCode": "LE",
"origin": [
{
"crs": "IPS",
"locationName": "Ipswich"
}
],
"serviceID": "ILZn7gyLj+eoZZfyaFlP0w==",
"std": "12:20"
}
The problem is that <OriginStation name={this.state.service.origin[0].crs} /> throws an error:
TypeError: undefined is not an object (evaluating 'this.state.service.origin')
I'm not sure why this isn't working, if I do console.log(dataObj.service.origin[0].crs) inside componentDidMount it outputs fine. I think it's something to do with the origin array...
Any help appreciated.
EDIT:
Screenshot of the state in the Chrome Inspector:
It's because your TrainService render method calls earlier than fetchService promise resolves.
Easiest way to fix your error is wait for fetchService updates service state:
var TrainService = React.createClass ({
getInitialState () {
return {
service: null
}
},
componentDidMount () {
fetchService()
.then(function (dataObj) {
this.setState({
service: dataObj.service
})
}.bind(this))
},
render () {
if (this.state.service === null)
return null;
return (
<section>
<DepartureTime time={this.state.service.std} />
<OriginStation name={this.state.service.origin[0].crs} />
</section>
)
}
})
fetchService is making an async call. So when componentDidMount is run, it will make an async call and proceeds to render.
When the render function is executed for the first time you state is not populated with the data and it has an empty array for this.state.service, from getInitialState.
If you are writing console.log inside componentDidMount as
componentDidMount () {
fetchService()
.then(function (dataObj) {
console.log(dataObj.service.origin[0].crs)
this.setState({
service: dataObj.service
})
}.bind(this))
},
the console.log gets executed only when the async call has succeeded, so the data is available.
To solve this either don't render the component until the state data is ready.
render() {
this.state.service.length == 0 && return null
...
}
Related
This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 3 years ago.
In React ... I am trying to read the response return from API and get undefined, what is the problem?
Undefined occurs when calling the function retrieveItems() from the component.
**// item service class**
import axios_o from 'axios';
class ItemService {
retrieveItems() {
axios_o.get("https://jsonplaceholder.typicode.com/posts")
.then(response => {
return response;
}).catch();
}
}
**// component calling the item service**
import React from 'react'
import ItemService from "../Services/ItemService";
class Posts extends React.Component {
constructor(props) {
super(props);
}
componentDidMount = () => {
this.itemservice=new ItemService();
**console.log(this.itemservice.retrieveItems())**
}
render() {
return (
<h1>Posts List</h1>
);
}
}
export default Posts;
class ItemService {
retrieveItems() {
return axios_o.get("https://jsonplaceholder.typicode.com/posts")
.then(response => response)
.catch(error => error)
}
}
componentDidMount = () => {
this.itemservice=new ItemService();
this.itemservice.retrieveItems().then(res=>{
console.log(res);
}).catch(error=>{
console.log(error)
});
}
As I mentioned in the comment the method retrieveItems is not returning a value. To fix this return the axios call
retrieveItems() {
return axios_o.get("https://jsonplaceholder.typicode.com/posts")
.then(response => {
return response;
}).catch(
);
}
or rewrite it to async/await for better readability
async retrieveItems() {
try {
return await axios_o.get("https://jsonplaceholder.typicode.com/posts")
}catch(e) {
// do some error handling or move the try/catch to caller side
}
}
Now in your console log you should see not the real response of the API call but a Promise. To get the real response you also have to wait for the answer on caller side:
class Posts extends React.Component {
constructor(props) {
super(props);
}
componentDidMount = () => {
this.retrieveItems()
}
retrieveItems = async () => {
this.itemservice=new ItemService();
const response = await this.itemservice.retrieveItems()
console.log(response)
}
render() {
return (
<h1>Posts List</h1>
);
}
}
With this you should see the response in the console log.
The issue is the typical pitfall of wanting to return something from within a callback function to the outer function. That's can't work, because the outer function (retrieveItems) has already finished. You need to stay in the asynchronous pattern. The easiest is probably this:
import axios_o from 'axios';
class ItemService {
retrieveItems() {
return axios_o.get("https://jsonplaceholder.typicode.com/posts");
}
}
import React from 'react'
import ItemService from "../Services/ItemService";
class Posts extends React.Component {
componentDidMount = () => {
this.itemservice = new ItemService();
this.itemservice.retrieveItems().then((res) => {
console.log(res);
});
}
render() {
return (<h1>Posts List</h1>);
}
}
export default Posts;
Component to test
class Carousel extends React.Component {
state = {
slides: null
}
componentDidMount = () => {
axios.get("https://s3.amazonaws.com/rainfo/slider/data.json").then(res => {
this.setState({ slides: res.data })
})
}
render() {
if (!slides) {
return null
}
return (
<div className="slick-carousel">
... markup trancated for bravity
</div>
)
}
}
export default Carousel
Test
import React from "react"
import renderer from "react-test-renderer"
import axios from "axios"
import Carousel from "./Carousel"
const slides = [
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
{
ID: "112",
REFERENCE_DATE: "2018-07-06",
...
},
...
]
jest.mock("axios")
it("", () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: slides }))
const tree = renderer.create(<Carousel />).toJSON()
expect(tree).toMatchSnapshot()
})
snapshot only records null, since at the moment of execution I suppose state.slides = null.
Can't put my finger on how to run expectations after axios done fetching the data.
Most of the samples online either use enzyme, or show tests with async functions that return promises. I couldn't find one that would show example only using jest and rendered component.
I tried making test function async, also using done callback, but no luck.
in short:
it("", async () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: slides }))
const tree = renderer.create(<Carousel />);
await Promise.resolve();
expect(tree.toJSON()).toMatchSnapshot()
})
should do the job
in details: besides you have mocked call to API data is still coming in async way. So we need toMatchSnapshot call goes to end of microtasks' queue. setTimeout(..., 0) or setImmediate will work too but I've found await Promise.resolve() being better recognizable as "everything below is coming to end of queue"
[UPD] fixed snippet: .toJSON must be after awaiting, object it returns will never be updated
The accepted answer started to fail the next day. After some tweaking, this seems to be working:
import React from "react"
import renderer from "react-test-renderer"
import axios from "axios"
import Carousel from "./Carousel"
jest.mock("axios")
const slides = sampleApiResponse()
const mockedAxiosGet = new Promise(() => ({ data: slides }))
axios.get.mockImplementation(() => mockedAxiosGet)
// eventhough axios.get was mocked, data still comes anychrnonously,
// so during first pass state.slides will remain null
it("returns null initally", () => {
const tree = renderer.create(<Carousel />).toJSON()
expect(tree).toMatchSnapshot()
})
it("uses fetched data to render carousel", () => {
const tree = renderer.create(<Carousel />)
mockedAxiosGet.then(() => {
expect(tree.toJSON()).toMatchSnapshot()
})
})
function sampleApiResponse() {
return [
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
]
}
I have a search component containing an input on which I defined a key up event handler function for fetching data based on entered string. As you can see below:
class SearchBox extends Component {
constructor(props) {
super(props);
this.state = {
timeout: 0,
query: "",
response: "",
error: ""
}
this.doneTypingSearch = this.doneTypingSearch.bind(this);
}
doneTypingSearch(evt){
var query = evt.target.value;
if(this.state.timeout) clearTimeout(this.state.timeout);
this.state.timeout = setTimeout(() => {
fetch('https://jsonplaceholder.typicode.com/todos/1/?name=query' , {
method: "GET"
})
.then( response => response.json() )
.then(function(json) {
console.log(json,"successss")
//Object { userId: 1, id: 1, title: "delectus aut autem", completed: false } successss
this.setState({
query: query,
response: json
})
console.log(this.state.query , "statesssAfter" )
}.bind(this))
.catch(function(error){
this.setState({
error: error
})
});
}, 1000);
}
render() {
return (
<div>
<input type="text" onKeyUp={evt => this.doneTypingSearch(evt)} />
<InstantSearchResult data={this.state.response} />
</div>
);
}
}
export default SearchBox;
The problem is the setState which I used in the second .then(). The response won't update . I want to update it and pass it to the InstantSearchResult component which is imported here. Do you have any idea where the problem is ?
Edit - Add InstantSearchResult component
class InstantSearchBox extends Component {
constructor(props) {
super(props);
this.state = {
magicData: ""
}
}
// Both methods tried but got error => Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
componentDidUpdate(props) {
this.setState({
magicData: this.props.data
})
}
shouldComponentUpdate(props) {
this.setState({
magicData: this.props.data
})
}
render() {
return (
<h1>{ this.state.magicData}</h1>
);
}
}
export default InstantSearchBox;
Edit:
Be aware that setState is asynchronous reading this article.
I've understand that the setState works fine in my fetch success the problem was the console.log which I shouldn't use it after setState instead I console.log in render() and I found out that the state updates correctly .
The other thing I wasn't careful for was the InstantSearchResult Constructor! When I re-render the SearchBox component consequently the InstantSearchResult renders each time but it's constructor runs just once. And if I use setState in InstantSearchResult I will face an infinite loop so I have to use this.props instead to pass the data to the second component.
this has been overridden inside the promise callback function. You to save it to a variable:
doneTypingSearch(evt){
var _this = this,
query = evt.target.value;
if(this.state.timeout) clearTimeout(this.state.timeout);
this.state.timeout = setTimeout(() => {
fetch('https://jsonplaceholder.typicode.com/todos/1/?name=query' , {
method: "GET"
})
.then( response => response.json() )
.then(function(json) {
console.log(json,"successss")
//Object { userId: 1, id: 1, title: "delectus aut autem", completed: false } successss
_this.setState({
query: query,
response: json
})
console.log(_this.state.query , "statesssAfter" )
}/*.bind(this)*/)
.catch(function(error){
_this.setState({
error: error
})
});
}, 1000);
}
I'm new to react and I'm trying to pull and display data from randomuserapi. I've made the api call but when I run my app, I get the error below:
./src/App.js
Line 45: 'getData' is not defined no-undef
Here's my code below: The getData() method is where I make the api call. That method is now called in ComponentDidMount.
I also binded getData() to my constructor but I still get the error above.
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
people: []
}
this.getData = this.getData.bind(this);
}
getData() {
const promise = fetch('https://randomuser.me/api/?results=20')
.then(response => {
if (response.status >= 400) {
throw `Response Invalid ( ${response.status} )`;
return;
}
return response.json();
})
.then(({results}) => {
return results;
})
.catch(error => {
console.log(error);
});
return promise;
}
ComponenDidMount() {
getData()
.then(data => {
this.setState({
people: data
});
});
}
render() {
return (
<div>
<p>{this.state.people.results[0].gender}</p>
</div>
);
}
}
export default App;
I'm also using create-react-app from Github. Please some assistance will be helpful.
Thanks!
When you reference defined methods you need to say this so:
componenDidMount() {
this.getData()
.then(data => {
this.setState({
people: data
});
});
}
Try adding this. when calling your functions.
this.getData() inside componentDidMount
How can I pass data I receive from a get request pass over to a component? Whatever I tried wouldn't work but my thinking was as the code below shows..
Thanks!
export function data() {
axios.get('www.example.de')
.then(function(res) {
return res.data
})
.then(function(data) {
this.setState({
list: data
})
})
}
import {data} from './api.js';
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ""
};
}
componentWillMount() {
data();
}
render() {
return <p > this.state.list < /p>
}
}
You call this.setState inside of data()->then callback, so this is context of the then callback function. Instead you should use arrow functions (it does not have its own context) and pass component's this to data function using call
export function data() {
axios.get('www.example.de')
.then(res => res.data)
.then(data => {
this.setState({
list: data
})
})
}
import {data} from './api.js';
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ""
};
}
componentWillMount() {
data.call(this);
}
render() {
return <p > this.state.list < /p>
}
}
However, your data services must not know about setState and, event more, expect passing this from react component. Your data service must be responsible for retrieving data from server, but not for changing component state, see Single responsibility principle. Also, your data service can be called from another data service. So your data service should return promise instead, that can be used by component for calling setState.
export function data() {
return axios.get('www.example.de')
.then(res => res.data)
}
and then
componentWillMount() {
data().then(data=>{
this.setState({
list: data
})
});
}
your api shouldn't know anything about your component, you can easily do this with callback, like so -
export function data(callback) {
axios.get('www.example.de')
.then(res => callback({ data: res.data }))
.catch(err => callback({ error: err }));
}
By doing this you can easily unit test your api
So in your Test component, you simply do -
componentWillMount() {
data(result => {
const { data, error } = result;
if (error) {
// Handle error
return;
}
if (data) {
this.setState({ list: data });
}
});
}
Your request is a promise so you can simply return that from the imported function and use the eventual returned result of that within the component. You only want to be changing the state of the component from within the component.
export function getData(endpoint) {
return axios.get(endpoint);
}
Note I've changed the name of the function to something more "actiony".
import { getData } from './api.js';
class Test extends React.Component {
constructor(props) {
super(props);
// Your state is going to be an array of things, so
// initialise it with an array to spare confusion
this.state = { list: [] };
}
// I use ComponentDidMount for fetch requests
// https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/
componentDidMount() {
// We've returned a promise from `getData` so we can still use the
// promise API to extract the JSON, and store the parsed object as the
// component state
getData('www.example.de')
.then(res => res.data)
.then(list => this.setState({ list }))
}
}
Your external function doesn't have the correct context of this, so you'll need to call it with the correct context from within the component:
componentWillMount() {
data.call(this);
}
However, inside the API call, it still won't have the correct this context, so you can set a variable to point to this inside the data() function:
export function data() {
let that = this;
axios('http://www.url.com')
.then(function(res) {
return res.data
})
.then(function(data) {
that.setState({
list: data
})
})
}
Details of the this keyword
However, it's generally considered better practice to only handle your state manipulation from with the component itself, but this will involve handling the asynchronous nature of the GET request, perhaps by passing in a callback to the data() function.
EDIT: Updated with asynchronous code
//api.js
data(callback){
axios.get('www.url.com')
.then(res => callback(res));
}
//component.jsx
componentWillMount(){
data(res => this.setState({list: res}));
}