Search code examples
javascriptreactjspromisereduxredux-thunk

Array created in a Redux thunk is not being passed properly to the State tree


I'm building a React/Redux project that makes a couple calls to an S3 bucket to grab images, and then render them into the app.

I'm finding that for some reason, I cannot iterate through the array that I set in my state tree as a result of those calls to render them onto the app. I believe that I may be dealing with an array-like object or something within the Promises I created caused this mutation to occur.

First, I have this file in a folder titled utils:

const bucketName = 'foo';
const roleARN = 'arn:aws:s3:::foo';
const identityPoolId = 'us-west-2:anActualIdHere';

AWS.config.update({
  region: 'us-west-2',
  credentials: new AWS.CognitoIdentityCredentials({
    IdentityPoolId: identityPoolId
  })
});

const bucket = new AWS.S3({
  params: {
    Bucket: bucketName,
  }
});

const textDecoder = new TextDecoder('utf8');

export function listObjects (callback) {
  return bucket.listObjects((error, data) => {
    if (error) {
      console.error('error: ', error);
      return;
    }

    callback(data.Contents);
  });
}

export function getSingleObject (key) {
  let getObject = new Promise((resolve, reject) => {
    bucket.getObject({
      Bucket: bucketName,
      Key: key
    }, (error, data) => {
      if (error) {
        console.error('error: ', error);
      }

      resolve(data.Body.toString('base64'));
    });
  })

  return getObject.then((result) => {
    return result;
  })
}

What happens here is that listObjects will return an array of all items in a specific S3 bucket.

Then, the getSingleObject function grabs a single object's contents based on a key provided in that list of all items, grabbing its Uint8Array and converting it to a base-64 string.

These two functions are called in a thunk action:

import { listObjects, getSingleObject } from '../utils/index.js';

export function loadPhotos () {
  return function (dispatch) {
    listObjects((results) => {
      let photos = [];

      let mapPhotosToArray = new Promise((resolve, reject) => {
        results.forEach((singlePhoto) => {
          let getSinglePhoto = new Promise((resolve, reject) => {
            resolve(getSingleObject(singlePhoto.Key));
          });

          getSinglePhoto.then((result) => {
            photos.push(result);
          });
        });
        resolve(photos);
      })

      mapPhotosToArray.then((result) => {
        dispatch(savePhotos(result));
      });
    });
  }
}

function savePhotos (photos) {
  return {
    type: 'GET_PHOTOS',
    photos
  }
}

loadPhotos is the thunk action exposed to my Redux containers. It returns a function that first calls the listObjects function in the utils file, passing it a callback that creates an array called photos.

Then, it creates a new Promise that loops through the results array returned by the listObjects utility function. In each iteration of this results array, I instantiate another new Promise that calls the getSingleObject utility.

Within that iteration, I push the results of getSingleObject into the photos array created within the callback passed into listObjects.

The last thing I do in the loadPhotos thunk action is call the outer Promise and then dispatch the result to the savePhotos action, which finally dispatches the payload object to the store for my reducer to catch.

This is how my reducer looks:

const defaultState = {
  photos: []
}

const photos = (state = defaultState, action) => {
  switch (action.type) {
    case 'GET_PHOTOS':
      return Object.assign({}, state, {
        photos: action.photos
      })
    default:
      return state;
  }
};

export default photos;

Here is how I've set up the entry point to the app:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, compose, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

import photos from './reducers/';

import App from './components/app';

import styles from './styles/styles.css';

const createStoreWithMiddleware = compose(applyMiddleware(thunk))(createStore);

const store = createStoreWithMiddleware(photos, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

ReactDOM.render(
  <Provider store={ store }>
    <App />
  </Provider>,
  document.getElementById('root')
)

This is the App component rendered:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

import AllPhotos from '../containers/AllPhotos';
import Navbar from './Navbar';

import '../styles/styles.css';

export default class App extends Component {
  constructor (props) {
    super (props);
  }

  render () {
    return (
      <div className="app">
        <Navbar />
        <AllPhotos />
      </div>
    )
  }
}

Here is the AllPhotos container it renders:

import { connect } from 'react-redux';

import AllPhotos from '../components/AllPhotos';

import {
  loadPhotos
} from '../actions/index.js';

const mapDispatchToProps = (dispatch) => {
  return {
    loadPhotos: () => {
      dispatch(loadPhotos());
    }
  }
}

const mapStateToProps = (state) => {
  return {
    photos: state.photos
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(AllPhotos);

And finally, this is the AllPhotos component:

import React, { Component } from 'react';
import _ from 'lodash';

export default class AllPhotos extends Component {
  constructor(props) {
    super(props);
  }

  componentWillMount () {
    this.props.loadPhotos();
  }

  render () {
    return (
      <div>
        <h1>Test</h1>
        { this._renderImages() }
      </div>
    )
  }

  _renderImages () {
    _.map(this.props.photos, (image) => {
      return (
        <img
          src={ 'data:image/png;base64,' + image }
          width={ 480 }
        />
      )
    })
  }
}

This is what happens when I attempt to log this.props.photos within _renderImages:

First Image

The first log occurs before the photos array is populated and loaded into my state.

However, if I were to log the length of this.props.photos in the same area that I log just the array itself, this is what I see:

enter image description here

When I call Array.isArray on this.props.photos in that same line, this is what I get:enter image description here

I have also attempted to convert this into an array using Array.from, but have not been successful.

Going a bit deeper, I attempted to find the length of the photos array from the action payload in my reducer, and still received 0 as the output. I also tried this within the savePhotos action, and found the same result.

As a result, I believe I may have not written my Promises correctly. Could someone help point me in the right direction?


Solution

  • You can try rewriting listObjects and getSingleObject to return Promises and utilising Promise.all.

    Then you can write loadPhotos

    export function loadPhotos () {
      return function (dispatch) {
        listObjects().then((results) => 
          Promise.all(results.map((singlePhoto) => getSingleObject(singlePhoto.Key)))
            .then((result) => dispatch(savePhotos(result)))
        )
      }
    }