Set React array state after promise all is completed

Viewed 328

I am new to React and javascript for that matter and am having trouble setting a components state. What I am trying to do is fetch a list of photos from an endpoint of mine. Once I have the photo list I need to map over the photos and call another endpoint to add some other meta data. My goal is to set the state of my component array once so I only render the webpage one time with all of the images.

I believe I need a Promise.All statement to determine when all of the images have returned from a fetch statement inside of a map. However I am not sure if this is the right approach or how it would look.

I will post some of my code below. Any and all recomendations are welcome, Thanks!

server.js

const express = require('express');
const app = express();
var AWS = require('aws-sdk');
var url = require('url');
var http = require('http');
var s3 = new AWS.S3({apiVersion: '2006-03-01'});
var url = require('url');
var http = require('http');
var sizeOf = require('image-size');
var params = {
    Bucket: "owenpersonalphotos",
};
const URL = "http://owenpersonalphotos.s3.amazonaws.com/";

app.get('/api/listPhotos', (req,res) => {
    console.log("GET /api/listPhotos")
    s3.listObjects(params, function (err, data) {
        res.json(data.Contents)
    });
});

app.get('/api/getPhoto/:key', (req, res) => {
    console.log("GET /api/getPhoto/"+req.params.key)
    let key = req.params.key;
    s3.listObjects(params, function (err, data) {
            let imgUrl = URL+key;
            options = url.parse(imgUrl);
            http.get(options, function (response) {
                var chunks = [];
                response.on('data', function (chunk) {
                  chunks.push(chunk);
                }).on('end', function() {
                  var buffer = Buffer.concat(chunks);
                  res.json(sizeOf(buffer))
                });
            })
    });
});

const port = 5000;
app.listen(port, () => console.log(`Server started on port ${port}`));

App.js

class App extends React.Component {
  constructor() {
    super();
    this.loadPhotos = this.loadPhotos.bind(this);
    this.state = { 
      Photos: []
    };
  }
  componentDidMount() {
    this.loadPhotos();
  }

  loadPhotos() {
    let p = []
    fetch('api/listPhotos')
        .then(res => res.json())
        .then(data => 
          { Promise.all(data.map(data => fetch("api/getPhoto/" + data.Key)
            .then(res => res.json())
            .then(getPhotoData => (p = ({
              src:"https://owenpersonalphotos.s3.amazonaws.com/"+data.Key,
              width: getPhotoData.width,
              height : getPhotoData.height,
              id: data.ETag
            },this.setState({Photos:p}))))))
          })
        }

  render() {
      return (
        <div className="App">
            {this.state.Photos}
        </div>
      );
  }
}
3 Answers

You could try something along the lines of this. Unfortunately I am unable to replicate your exact scenario so it might not work, but here's how you could resolve a Promise.all.

const initialiser = fetch('api/listPhotos')
    .then(res => res.json())
    .then(data => {  // map every photo request to the promise of the fetch
        let requests = data.map(data => fetch("api/getPhoto/" + data.Key)
         .then(res => res.json())
         .then(getPhotoData => ({
            src: "https://owenpersonalphotos.s3.amazonaws.com/" + data.Key,
            width: getPhotoData.width,
            height : getPhotoData.height,
            id: data.ETag
        )}))

        // Promise.all waits until all jobs are resolved

        Promise.all(requests)
         .then(responses => this.setState({ Photos: responses });

      })
    })

I would use React Hooks instead for functional components and storing state into an array

import React, { useState, useEffect } from "react";

State set as empty array below:

const [photos, setPhotos] = useState([]);

This following effect will run getPhotos() once on page load

 useEffect(() => {
    getPhotos();
  }, []);

getPhotos from API and map to state photos

const getPhotos = async () => {
    //async way
    try {
        const response = await fetch("YOUR API URL");
        let photos = await response.json;
        setPhotos(photos);
    } catch (err) {
        console.log(err);
    }

       /* with just promises
    return fetch("YOUR API URL")
  .then(response => response.json())
  .then(response => {
    console.log(response);
    setPhotos(response);
  });
  */

and then you can just map it in the return statement:

<div className="photos-container">
  {photos.map((p, i) => {
     return (
        <div className="photo" key={i}>
           <img src={photos.imgSrc} alt={photos.name}>
        </div>
     );
   })}
</div>

Hope this helps I know it's not your example exactly

Your fetches were look like a promise hell situation, I will make it a bit clearer like this:


loadPhotos() {
    let promises = [];
    fetch('api/listPhotos')
    .then(res => res.json())
    .then(list => {
        for (let photo of list) {
            const aCall = fetch("api/getPhoto/" + photo.Key).then(res => res.json());
            promises.push(aCall);
        }
        return Promise.all(promises);
    }).then(detailedPhotos => {
        const Photos = detailedPhotos.map(item => ({
            src: "https://owenpersonalphotos.s3.amazonaws.com/" + item.Key,
            width: item.width,
            height : item.height,
            id: item.ETag
        }));
        this.setState({ Photos });
    })
}
render() {
    return (
        <div className="App">
            {this.state.Photos.map(p => <div>{p.id}</div>)}
        </div>
    );
}

Related