Daniel Ertel-Moore
Daniel Ertel-Moore

Reputation: 91

Having trouble with React rendering a series of images from a map function

I'm trying to design a component where the user can click a button which will trigger a giphy API call that will eventually render a series of gif images.

So far I'm able to successfully get everything done except the actual image rendering. Here's my code so far:

retrieveURLs() {
    *bunch of stuff to make API call and return an array of URLs related 
    to the category of the button the user pushes* 
}
renderGifs() {
    this.retrieveURLs().then(function(results) {
        console.log(results); //logs array of URLs  
        return results.map(function(url, index) {
            console.log(url); //logs each url
            return (<img key={index} src={url} alt="" />)
        }, this);   
    });
}
render() {
    return(
        <div id="gif-div">
            {this.renderGifs()}
        </div>
    )
}

Nothing gets rendered despite each console.log() event indicating that the URL(s) are at least being passed properly.

I do something similar for the parent component to render the buttons from an array of categories that looks like this:

btnArray = [*bunch of categories here*];
renderButtons() {
    return btnArray.map(function(item, i) {
        let btnID = btnArray[i].replace(/\s+/g, "+");
        return <button type='button' className="btn btn-info" onClick={this.handleCategorySelect} key={i} id={btnID} value={btnID}>{btnArray[i]}</button>
    }, this)
}

The buttons are rendered properly, but my images are not. Neither the renderbuttons nor the rendergifs metohds alter the state. Honestly I can't see a meaningful difference between the two, so I'd like to have some help figuring out why one works but the other doesn't.

Upvotes: 2

Views: 1215

Answers (3)

Tomasz Mularczyk
Tomasz Mularczyk

Reputation: 36169

first of all you forgot the return statement:

renderGifs() {
    return this.retrieveURLs().then(function(results) {
...
}

but this won't solve anything as it is returning a Promise.

You need to save request results in the state and then map it:

constructor(props){
  super(props);

  this.state = { images: [] };
}

componentDidMount() {
   this.renderGifs();
}

renderGifs() {
    this.retrieveURLs().then(function(results) {
        console.log(results); //logs array of URLs  
        this.stateState({ images: results }); 
    });
}

render() {
    return(
        <div id="gif-div">
            {
              this.state.images.map((url, index) => (<img key={index} src={url} alt="" />);
            }
        </div>
    )
}

Upvotes: 0

philraj
philraj

Reputation: 841

This is the nature of asynchronous functions; you can't return a value from within a callback to the original call site. If you were to write:

const res = this.retrieveURLs().then(function(results) {
    return results;
});

you'd only be changing the resolution value of the promise. res won't be assigned the value of results, but rather it will be assigned the promise created by this.retrieveURLs(), and the only way to retrieve the value of a resolved promise is by attaching a .then callback.


What you could do is this:

this.retrieveURLs().then(results => {
    this.setState({ images: results });  
});

Now your internal state will be updated asynchronously, and your component will be told to re-render with the new data, which you can use in your render function by accessing the state.

Note: I'm using an arrow function to create the callback in the above example, because otherwise this won't be bound to the right context. Alternatively, you could do the old that = this trick, or use Function#bind.

Upvotes: 3

blackeyebeefhorsefly
blackeyebeefhorsefly

Reputation: 1949

The problem lies with the rendering function for the images and the way React does diffing between the previous and current state. Since the fetch is asynchronous and the render is not, when the fetch is completed React doesn't know that it needs to re-render you component. There are ways to force a re-render in this case, but a better solution is to use the functionality that's already part of the shouldUpdate check.

Your implementation might change to look something like the following:

class Images extends Component {
  state = {
    images: [],
    error: null
  }

  componentDidMount() {
    return this.retrieveImages()
      .then(images => this.setState({images}))
      .catch(error => this.setState({error}))
  }

  ... rest of your class definition

  render () {
    return (
      <div>
        {this.state.images.map(image => <img src={image.url} />)}
      </div>
    )
  }
}

I would also have some handling for bad results, missing key / values, etc. I hope that works for you, if not let me know! :)

Upvotes: 2

Related Questions