sidp sidp - 3 months ago 9
React JSX Question

React/Redux can't iterate over array in state

I'm in the process of learning React/Redux and I've run into an issue while converting a single page web app of mine to the framework/paradigm. What i'm trying to do is let the initial state of the web app have an array that is to be populated by objects from an API request, this array is called "makes". I want to do this so I can display "makes" from the API on the first page of the website upon it loading. This can be seen in the

index.js
file below:

import App from './App';
import './index.css';
import configureStore from './redux/store'
import { Provider } from 'react-redux'

let makesUrl = 'the url to the API'
let cached = cache.get(makesUrl)
let makes = []

// Future cache setup.
if(!cached) {
console.log("NOT CACHED")
}
else {
console.log("CACHED")
}

// Get the makes via API.
fetch(makesUrl).then((response) => {
// Pass JSON to the next 'then'
return response.json()
}).then((json) => {
json.makes.forEach((make) => {
makes.push(make)
})
})

let initialState = {
grids: [],
makes: makes
}

let store = configureStore(initialState)

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


The
state
and
dispatch
are mapped to the props and passed down to the components that need them in my
App.js
file as such:

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import './App.css'
import Head from './components/Head'
import Middle from './components/Middle'
import Foot from './components/Foot'
import actions from './redux/actions'

class App extends Component {

render() {
return (
<div className="App">
<div>
<Head />
<div className="middle container">
<Middle actions={this.props.actions} makes={this.props.makes}/>
</div>
<Foot />
</div>
</div>
);
}
}

function mapStateToProps(state) {
return state
}

function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
}

export default connect(mapStateToProps, mapDispatchToProps)(App)


At all points, in the chrome dev tools, I can see that the API call was successful and the state is shown to have
makes: Array[62]
with 62 objects inside, however if I console log the length of the array in the component that these makes are passed down to as seen below, it says the length is 0, and each index of the array is undefinded.

import React, { Component } from 'react'

class MakeButtons extends Component {

handleClick(event) {
event.preventDefault()
console.log("CLICK")
}

render() {
return(
<div>
{
console.log(this.props.makes.length)
}
</div>
)
}
}

export default MakeButtons


This is essentially what I've been trying to figure out for the past couple hours, so I can use the
forEach
or
map
function to return links/buttons for each of the objects in the array, however at the moment this does not work, despite dev tools showing the state to be normal. Any help/explanations would be greatly appreciated!

Answer

So you really just need to set up an action/reducer for your init, then you can call it in componentWillMount or componentDidMount because they are only called once upon loading your app.

In the way you are doing it now you have a fetch and an app using the data from the fetch that is not waiting for the async call to finish before it starts the app.

You just want to create your init action, so your action creator would be something like :

   import * as services from './services';

   function initMyAppDispatcher(type, data, status) {
       return {
          type, 
          data,
          status
        };
   }


   export function initMyApp(dispatch, makesUrl) {
      dispatch(initMyAppDispatcher(actions.INIT_APP, {}, 'PENDING');

      return services.myInitCall(makesUrl)
         .then((data) =>
             dispatch(initMyAppDispatcher(actions.INIT_APP, data, 'SUCCESS'),
            (error) =>
             dispatch(initMyAppDispatcher(actions.INIT_APP, error, 'FAILURE'),
         )
         .catch(yourErrorHandling);
   }

Services.myInitCall is however you want to implement it, just make sure you export it back as a promise. In your case you can replace that line with fetch(makesUrl) as long as you have access to it there. Then having it set up like this, you can set your reducers like so :

   case actions.INIT_APP:
            if (action.status) {
                switch (action.status) {
                    case PENDING:
                        //you can use this to create a "loading" state like a spinner or whatever
                        return state;
                    case SUCCESS:
                        // note: this is immutablejs syntax, use whatever you prefer
                        return state.set('makes', action.data);
                   case FAILURE:
                        return state;

                    default:
                        return state;
                }
            }
            return state;

One thing to note is I have dispatch in my action creators because I use mapDispatchToProps in place of mapToProps. So your container looks something like this :

import * as actionCreators from './action-creators';

function mapStateToProps(state) {

    return {
        makes: state.get('makes')
    };
}

function mapDispatchToProps(dispatch, ownProps) {
    return {
        initMyApp: actionCreators.initMyApp.bind(null, dispatch)
    };
}


export default function(component = Component) {
    return connect(mapStateToProps, mapDispatchToProps)(component);
}

then in your component componentWillMount or componentDidMount, pass in and call your init function

   componentDidMount() {
       this.props.initMyApp();

   }