0

i am new to React and Redux. Trying to understand the basics and do some simple examples, but i am stuck in this problem for more than one day i can't find the sollution. I imagine that my mistake is a dumb mistake.

The problem is that i can't print the array of users. When debugging, the variable users is loading with all the corrected ids and users, but after executing the <li key={id}>{name}</li> for three times, it comes back to the forEach and gives me this exception: Uncaught TypeError: Cannot read property 'forEach' of undefined, where users is undefined. And i also get an error corresponding to the PropTypes: Invalid prop user of type array supplied to HomePage, expected object

Here is the code:

store/configureStore.js

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers/index';

const initialState = {};

const middleware = [thunk];

const store = createStore(rootReducer, initialState, compose(
    applyMiddleware(...middleware),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
));

export default store;

reducers/index.js

import { combineReducers } from 'redux';
import userReducer from './userReducer';
//import groupReducer from './groupReducer';

export default combineReducers({
    user: userReducer
});

reducers/userReducer.js

import { GET_USERS, ADD_USER, DELETE_USER } from '../actions/types';

const initialState = {
    users: [
        { id: 1, name: 'brunao'},
        { id: 2, name: 'flavio'},
        { id: 3, name: 'dudu'}
    ]
};

export default function(state = initialState, action) { 
    switch (action.type) {
        case GET_USERS:
            return [
                ...state
            ];
        default:
            return state;
    }
}

actions/usersAction.js

import { GET_USERS, ADD_USER, DELETE_USER } from './types';

export const getUsers = () => {
    return {
        type: GET_USERS
    };
};

components/HomePage.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getUsers } from '../actions/usersActions';
import PropTypes from 'prop-types';

class HomePage extends Component {

    componentDidMount() {
        this.props.getUsers();
    }

    render() {
        const { users } = this.props.user;
        return(
            <div>
                <h3>Users</h3>
                <ul>
                    {users.forEach(({id, name}) => (
                        <li key={id}>{name}</li>
                    ))}
                </ul>
            </div>
        );
    }

}

HomePage.propTypes = {
    getUsers: PropTypes.func.isRequired,
    user: PropTypes.object.isRequired
}

const mapStateToProps = (state) => ({
    user: state.user
});

export default connect(mapStateToProps, { getUsers })(HomePage);
Bruno Paiva
  • 31
  • 1
  • 5

2 Answers2

2

You are returning a wrong shape of state in your reducer. Your related code:

export default function(state = initialState, action) { 
    switch (action.type) {
        case GET_USERS:
            return [
                ...state
            ];
        default:
            return state;
    }
}

Here, state is an object but you are returning it in an array by spreading it. So, your state gets broken.

Try it like that:

case GET_USERS:
    return state;

As @Idan Dagan pointed out in his answer, actually we do not mutate state in our reducers. I just gave this suggestion since you are just playing around to learn Redux and we are returning the original state here, nothing more. But, this is a suitable and better way to return the state:

case GET_USERS:
    return { ...state };

Here is a working code: https://codesandbox.io/s/k5ymxwknpv

I also changed forEach with map again as @Idan Dagan suggested. I haden't realized that. forEach is not the suitable method here since actually it does not return anything. You want to map through your arrays in React and render them.

Also, your state name is confusing :) user.users is a little bit weird, you can think a better one maybe.

Edit after comments

Your GET_USERS action actually is being hit, but you are checking it wrong in your code. You are doing:

export default function(state = initialState, action) {
  switch (action.type) {
    case GET_USERS:
      return Object.assign({}, state);
    default:
      return {
        users: [
          { id: 1, name: "TEST" },
          { id: 2, name: "TEST1" },
          { id: 3, name: "TEST2" }
        ]
      };
  }
}

What happens here? First action of Redux is INIT. This is the initialization of your state. Now, since there is no certain action, your reducer hits the default case and returns the TEST one. Now, your state becomes this TEST data. Then your GET_USERS is hit and you return a new object which merges the state which is the TEST one. Here is the steps:

First, state is the `initialState` -> `INIT` runs and hits the default case
State is now TEST one -> GET_USERS hit and returns the `state` which is TEST one
You see the TEST one.

How can you test your actions? Just put a console.log in your reducer:

export default function(state = initialState, action) {
  console.log("state",state);
  console.log("action",action);
  .....

and see GET_USERS actually is being hit. The other option is instead of returning the merged object with state, try to merge it with initialState or with spread operator return a new object by using initialState:

return return Object.assign({}, initialState);

or

return {...initialState}

Last option provides you a little bit more understanding how my first explanation works. Try to return this for your GET_USERS:

return {...state, users:[...state.users, {id:4, name: "foo"}]};

You will see a users list with TEST data but the last one will be your foo. This explains how you loose your initialState if you return anything beside state in your default case.

Last suggestion, you can debug your Redux development with Redux Dev Tools. It is a great tool and does much more than debugging. You can easily track all your operations for Redux.

devserkan
  • 16,870
  • 4
  • 31
  • 47
  • Yeah, that correction inside the *return* this stop the error, now i am getting the users. But i made a test and i am getting the users because of the *default* option inside the *switch*, i am not dispatching the action *GET_USERS*.. I tried to change the *ComponentDidMount()* to *ComponentWillMount()* but no success – Bruno Paiva Jul 21 '18 at 20:11
  • @BrunoPaiva "ComponentWillMount" is considered legacy and you should avoid them. – Idan Dagan Jul 21 '18 at 20:19
  • DidMount or WillMount does not have any effect on this. And as told, WillMount is legacy now.How do you track your actions? Are you sure it is not hit? Can we see your `types` file? Also, can you put your whole app to https://codesandbox.io so we can examine it. – devserkan Jul 21 '18 at 20:25
  • I've edited my answer and added a working codesandbox example. Also, I've changed `forEach` with `map` according to @IdanDagan's suggestion. Good catch! – devserkan Jul 21 '18 at 20:53
  • https://codesandbox.io/s/my6k6o037p Here is the code.. i changed the return of *default* inside the *switch* and confirm that the action *GET_USERS* is not being called.. And thanks both of you for the help!! Really appreciate! – Bruno Paiva Jul 21 '18 at 23:32
  • I've edited my answer and tried to explain how your GET_USERS actually is being hit. – devserkan Jul 22 '18 at 00:54
  • @devserkan it is better to debug actions via Redux DevTool for chrome. Anyway, i gave you upvoting :). – Idan Dagan Jul 22 '18 at 05:12
  • 1
    @IdanDagan, I totally agree with you. I use Redux Dev Tools whenever I use Redux, it is a great tool. Here, I just want to show a simple way since OP is trying to debug the problem in the reducer. Anyways, I've edited my answer and mentioned from Redux Dev Tools. Thanks. – devserkan Jul 22 '18 at 15:00
  • Thank you guys for this incredible lesson :) I didn't know about the *INIT* action being called first! Now redux is more clear to me.. Again, thanks and have a good day. – Bruno Paiva Jul 22 '18 at 15:54
0

I'm not sure what are you trying to do with the new state.

But there is couple of changes that you need to do:

  1. Use map instead of forEach (because you want to return a new array).
  2. At the reducer you can return the state like this:

export default function(state = initialState, action) { 
    switch (action.type) {
        case GET_USERS:
            return Object.assign({}, state);
        default:
            return state;
    }
}

as mention in redux docs:

We don't mutate the state. We create a copy with Object.assign().

Idan Dagan
  • 10,273
  • 5
  • 34
  • 41