简体   繁体   中英

ReactJs re-render on state change

I am trying to do a sequence of fetching some API data and manipulating it as follows:

  1. Fetch list of string addresses via API
  2. Convert each of this string addresses to a geocoded location via API
  3. Displaying these geocoded addresses as markers on a map

I am having some trouble with getting the timing with all these asynchronous activities right (I am pretty new to Javascript).

Here's what I have so far:

class Map extends Component {
    constructor(props) {
        super(props);

        this.state = {
            addresses = [],
            geocodedAddresses = []
        }
    }

    componentDidMount() {
        const geocodedAddresses = []
        fetch('.....')
            .then(result => result.json())
            .then(addresses => this.setState({ addresses }, function() {
                this.state.addresses.forEach(address => {
                    Geocode.fromAddress(address).then(geocodedAddress => {
                        geocodedAddresses.push(geocodedAddress['results'][0]['geometry']['location'])
                    })
                })
            }))
            console.log(geocodedAddresses) //Correctly contains the geocoded addresses
            this.setState({ geocodedAddresses })
        }
    }

    render() {
        this.state.addresses.map(a => console.log(a)) //Evaluates correctly
        this.state.geocodedAddresses.map(ga => console.log(ga)) //Yields nothing....
    .....
    }
}

So I don't quite understand why React does not re render when I do this.setState({ geocodedAddresses }) - Shouldn't react re render when I do setState?

There are a couple errors with your code. In the first place, the state object is being created with equals instead of colons:

this.state = {
    addresses: [],
    geocodedAddresses: []
}

In the second place, you should take into account that your code is asynchronous. Only when the promises generated by the call to Geocode.fromAddress resolve you have the data for your geocodedAddresses .

In the componentDidMount , you are console logging the geocodedAdresses and you report that you are seeing the right values. This is only because the log is update after the promises resolve. But when you do the console.log the value at that moment for geocodedAdresses is an empty array. And that is the value that is being inserted in the component state .

In order to set the correct value for the state, you should call setState when all your Geocode.fromAddress promises have resolved. In order to do that you can use Promise.all method.

So, your code would look like:

class Map extends Component {
    constructor(props) {
        super(props);

        this.state = {
            addresses: [],
            geocodedAddresses: []
        }
    }

    componentDidMount() {
        fetch('.....')
            .then(result => result.json())
            .then(addresses => {
                Promise.all(addresses.map(address => Geocode.fromAddress(address)))
                    .then(geocodedAddresses => {
                        this.setState({
                            addresses,
                            geocodedAddresses
                        })
                    });
            }))
        }
    }

    render() {
        this.state.addresses.map(a => console.log(a)) //Evaluates correctly
        this.state.geocodedAddresses.map(ga => console.log(ga)) //Yields nothing....
    .....
    }
}

Note that with this solution setState is only being called once.

Since all your state refers to addresses information, it could make sense to merge that information into a single key in the state . You could initialize your state in the constructor as:

this.state = {
    addresses: []
};

And then, you could populate the state once all the promises resolve:

componentDidMount() {
    fetch('.....')
        .then(result => result.json())
        .then(addresses => {
            Promise.all(addresses.map(address => Geocode.fromAddress(address)))
                .then(geocodedAddresses => {
                    const addressesState = addresses.map((address, i) => {
                        return {
                            address,
                            geocodedAddress: geocodedAddresses[i]
                        };
                    });
                    this.setState({ addresses: addressesState })
                });
        }))
    }
}

You're right it's the async is slightly off. The console.log will populate after it "appears" in the console, but when setState has been called (and also when console.log has been called) its value is still [] .

if you're using async/await you can wait for the fetch chain to completely finish, or put the setState within the then . With the setState callback, it's probably better to do the latter.

componentDidMount() {
  const geocodedAddresses = []
  fetch('.....')
  .then(result => result.json())
    .then(addresses => this.setState({ addresses }, function() {
      this.state.addresses.forEach(address => {
        Geocode.fromAddress(address).then(geocodedAddress => {
          geocodedAddresses.push(geocodedAddress['results'][0]['geometry']['location'])
        })
      })
   console.log(geocodedAddresses) //Correctly contains the geocoded addresses
   this.setState({ geocodedAddresses })
}))

}}

or

componentDidMount = async () => {
      const geocodedAddresses = []
      let addresses = await fetch('.....').then(result => result.json())
      addresses.forEach(address => { // assuming this is sync
            Geocode.fromAddress(address).then(geocodedAddress => {
              geocodedAddresses.push(geocodedAddress['results'][0]['geometry']['location'])
            })
          })
       this.setState({ geocodedAddresses,addresses })

    }}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM