James
James

Reputation: 461

Reducer Action Not Dynamically Updating

I'm creating a basic CRUD app using React/Redux with a Rails API, and when I submit a car on my car-form, I get an error message - but refreshing the browser shows the car.

The error says Uncaught TypeError: Cannot read property 'map' of undefined on line 20 of my Cars.js file:

    import React, { Component } from 'react';
import { connect } from 'react-redux';

import CarCard from '../components/CarCard';
import CarForm from './CarForm';
import './Cars.css';
import { getCars } from '../actions/cars';


class Cars extends Component {

componentDidMount() {
    this.props.getCars()
}

render() {
    return (
    <div className="CarsContainer">
        <h3>Cars Component</h3> 
        {this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}
        <CarForm />
    </div>
    );
}

}

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

export default connect(mapStateToProps, { getCars })(Cars);

Here's my createCar action creator:

const addCar = car => {
return {
    type: 'CREATE_CAR_SUCCESS',
    car
}}

And my createCar async action:

export const createCar = car => {
return dispatch => {
    return fetch(`${API_URL}/cars`, {
        method: "POST",
        headers: {
            'Content-type': 'application/json'
        },
        body: JSON.stringify({ car: car })
    })
    .then(response => {
      try {
        return response.json()
      } catch(error) {
        console.log(error);
      }
    })
    .then(cars => {
        dispatch(addCar([car]));
        dispatch(resetCarForm())
    })
    .catch(error => console.log(error + 'createCar POST failed'))
}}

I'm not sure what's going wrong here, seeing as the app reflects my changes after I reload. Ultimately I'm trying to show that information without having to refresh the page.

Upvotes: 2

Views: 43

Answers (3)

victor.ja
victor.ja

Reputation: 888

The problem is that when your component mounts, it doesn’t have the cars array and instead it has an undefined value. This happens because getCars() is asynchronous.

Solution 1: add a defaultProp to the component:

Component.defaultProps = {
cars: { cars: [] }
}

Solution 2: Add a cars key to the reducer’s initialState

initialState: { cars:{ cars:[] } }

Upvotes: 1

Hemadri Dasari
Hemadri Dasari

Reputation: 33984

You are doing action call getCars in componentDidMount and this lifecycle method gets called after first render so on Initial render this.props.cars will be undefined

If you are getting this.props.cars like

   {
       “cars”: [....]
    }

Then you need to do conditional check before accessing cars object

Change

       {this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}

To

         {this.props.cars && this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}

Upvotes: 1

mikeb
mikeb

Reputation: 11267

You are rendering before your async action puts the values in the state. Try returning null from render if your state is not set yet:

render() {
    if(!this.props.cars.cars){
      return null;
    }
    return (
    <div className="CarsContainer">
        <h3>Cars Component</h3> 
        {this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}
        <CarForm />
    </div>
    );
}

In other words, if your state does not have a list of things to render return null - I think the above if will work, but you might want to console.log("Cars in render", this.props.cars) to see what you are getting.

The better option, IMO, is to set your initial state so that this.props.cars is [] and then you don't have to return null and have a special case in your render method. I would need to see your reducer to suggest how to do that, but if you make it have a sensible default/initial state you should be able to easily do this.

Upvotes: 1

Related Questions