1

I have a Node API that returns data in a JSON format and sends it to a React Component App that stores it in its state (nearbyShops) to pass it afterwards to the NearbyShop component props. The thing is I don't get the content displayed even though I used componentDidMount.

App.js:

import React, { Component } from 'react';
import axios from 'axios';
import './App.css';
import NearbyShop from './NearbyShop';

class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            lat: 0,
            long: 0,
            nearbyShops: []
        }
    }

    componentDidMount() {
        var that = this;
        // Get the user's location (lat & long) in the state object
        const url = 'https://freegeoip.net/json/';
        axios.get(url)
            .then((response) => response.data)
            // Get the long & lat of the end-user
            .then((data) => that.setState({
                                lat: data.latitude,
                                long: data.longitude
            }))
            // Call our built-in Nearby API with the lat & long of the end-user
            .then(function (data) {
                axios.get('/nearby', {
                    params: {
                        lat: that.state.lat,
                        long: that.state.long
                    }
                })
                .then((response) => that.setState({
                    nearbyShops: response.data.nearbyShops
                }))
                .catch(function (error) {
                    console.log(error);
                });
            })            
            .catch(function (error) {
                console.log(error);
            });
    }

  render() {
    return (
      <div className="App">
            <h1>Shops List</h1>
            <NearbyShop shopsList={this.state.nearbyShops} />
      </div>
    );
  }
}

export default App;

NearbyShop.js:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import {
    Card, Button, CardImg, CardTitle, CardText, CardGroup,
    CardSubtitle, CardBody
} from 'reactstrap';


class NearbyShop extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            shopsList: props.shopsList
        }
    }

    componentDidMount() {
        const shopsList = this.props.shopsList;
        this.setState({
            shopsList: shopsList
        });
    }

    render() { 

        if (this.state.shopsList.length == 0)
            return null;

        let shops = this.state.shopsList && this.state.shopsList.map(function (shop, i) {
            <Card key={i}>
                <CardImg top width="100%" src={shop.picture} alt="Card image cap" />
                <CardBody>
                    <CardTitle>{shop.name}</CardTitle>
                    <CardSubtitle>Card subtitle</CardSubtitle>
                    <CardText>{shop.email}</CardText>
                    <Button>Fall in love</Button>
                </CardBody>
            </Card>
        })

        return (
            <CardGroup>
                {shops}
            </CardGroup>
    );
    }
}

export default NearbyShop;
devio
  • 1,147
  • 5
  • 15
  • 41

2 Answers2

1

The NearbyShop component is mixing state and props.

NearbyShop doesn't need to manage state as the data is delivered from the parent component via props. It will re-render when the parent supplied props change from the API requests.

The current code set's the state at component initialisation, which never updates as the component is not initialised or mounted again, only rendered. So only the initial state, the empty array [] is all that is rendered.

class NearbyShop extends React.Component {
    render() {
        const { shopsList } = this.props
        if (!shopsList || shopsList.length == 0)
            return null;

        const shops = shopsList.map(function (shop, i) {
            <Card key={i}>
                <CardImg top width="100%" src={shop.picture} alt="Card image cap" />
                <CardBody>
                    <CardTitle>{shop.name}</CardTitle>
                    <CardSubtitle>Card subtitle</CardSubtitle>
                    <CardText>{shop.email}</CardText>
                    <Button>Fall in love</Button>
                </CardBody>
            </Card>
        })

        return (
            <CardGroup>
                {shops}
            </CardGroup>
        );
    }
}

Additionally, as Kryten noted, the App API requests are mixing promises and setState callbacks. This could cause your API requests to run out of sync with your state updates and you end up with inconsistent results rendered. The state changes can be batched together at the end for a single update/render.

componentDidMount() {
    // Get the user's location (lat & long) in the state object
    const url = 'https://freegeoip.net/json/';
    const state_updates = {}
    axios.get(url)
        .then((response) => {
            const data = response.data
            // Get the long & lat of the end-user
            state_updates.lat = data.latitude
            state_updates.long = data.longitude
            return axios.get('/nearby', {
                params: {
                    lat: state_updates.lat,
                    long: state_updates.long
                }
            })
        })
        .then((response) => {
            state_updates.nearbyShops = response.data.nearbyShops
            this.setState(state_updates)
            return true
        })
        .catch(function (error) {
            console.log(error);
        })
}

This code still doesn't wait for setState to finish but now it's the last piece of work being done and the promise flow is not relying on asynchronous changes in the component state any more.

Matt
  • 68,711
  • 7
  • 155
  • 158
-1

The problem likely stems from the fact that setState does not return a promise - it requires a callback if you wish to use the update state values after update.

So when you attempt to get the nearby locations, you will probably fail:

axios.get(url)
        .then((response) => response.data)
        // Get the long & lat of the end-user
        .then((data) => that.setState({
                            lat: data.latitude,
                            long: data.longitude
        }))
        // Call our built-in Nearby API with the lat & long of the end-user
        .then(function (data) {

            // data is undefined here, since it was returned from
            // setState. In addition, there is no guarantee that
            // the state has been updated and your lat & long values
            // are probably not accurate

            axios.get('/nearby', {
                params: {
                    lat: that.state.lat,
                    long: that.state.long
                }
            })

        })

The documentation for setState says:

Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.

Kryten
  • 15,230
  • 6
  • 45
  • 68
  • I get the nearby locations as expected (as a JSON). My challenge now is how to re-render the component NearbyShop to consider the response from App.js Also, my `long` and `lat` variables are filled correctly with the right values. – devio Mar 06 '18 at 00:32