I'm adding redux to an existing app, and I have trouble updating the state of a component which is subscribed to the store. Minimal chunk with my setup:
DivsContainer.js
const DivsContainer = React.createClass({
propTypes: {
collections : PropTypes.array.isRequired
},
render() {
return (
<div onClick={this.props.onClick}>
{this.props.collections.map((coll, i) => (
<div
key={coll.id}
name={coll.name}
/>
))}
</div>
)
}
})
function mapStateToProps(state, ownProps) {
return {
collections: state.collectionsReducer.collections,
}
}
function mapDispatchToProps (dispatch, ownProps) {
return {
onClick: () => {
dispatch(addCollection())
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(DivsContainer)
Reducers.js
import {combineReducers} from 'redux'
import {ADD_COLLECTION, REMOVE_COLLECTION} from './actions'
const initialState = {
collections: [
{
id: 1,
name: "mock",
}
}
]
}
function collectionsReducer(state = initialState, action) {
switch (action.type) {
case ADD_COLLECTION:
return [
...state,
{
id: action.id,
name: action.name,
}
]
default:
return initialState
}
}
const rootReducer = combineReducers({collectionsReducer})
export default rootReducer
actions.js
export const ADD_COLLECTION = 'ADD_COLLECTION'
let nextCollectionId = 2
export function addCollection() {
return {
type: ADD_COLLECTION,
id: nextCollectionId++,
name: 'mock',
}
}
The reducer is called, so I suspect the problem occurs when returning the new state object (reducers is incorrect) because I get:
Uncaught TypeError: Cannot read property 'map' of undefined render
#DivsContainer.js:
Your reducer is kind of messed up. collectionsReducer returns an array but your initialState is an object with an array in it.
The reducer probably should be:
return {
...state,
collections: [...state.collections, {id: action.id, name: action.name}],
};
and your mapStateToProps should be:
function mapStateToProps(state, ownProps) {
return {
collections: state.collections,
};
}
because you're mapping state to props and your state has the shape of {collections: []} not {collectionsReducer: collections: []}
It is because in your reducer, ADD_COLLECTION is returning an array [something], it is not {collections: something}. So the reducer does not have collections any more, and it complains 'map' of undefined . You need to return {collections: [something]} in your ADD_COLLECTION
Related
I have created a simple blog app using react and redux, right now only two functionalities are present in the app -> rendering and deleting blogs. Rendering is working fine but not the delete one.
Most likely, the issue will be in the reducer of my application. Please Suggest.
Here's my whole code.
sandbox link: https://codesandbox.io/s/testing-75tjd?file=/src/index.js
ACTION
import { DELETE_BLOGS, GET_BLOGS } from "./actionTypes";
// for rendering list of blogs
export const getBlog = () => {
return {
type: GET_BLOGS,
};
};
// for deleting the blogs
export const deleteBlogs = (id) => {
return {
type: DELETE_BLOGS,
id,
};
};
REDUCER
import { DELETE_BLOGS, GET_BLOGS } from "../actions/actionTypes";
const initialState = {
blogs: [
{ id: 1, title: "First Blog", content: "A new blog" },
{ id: 2, title: "Second Blog", content: "Just another Blog" },
],
};
const blogReducer = (state = initialState, action) => {
switch (action.types) {
case GET_BLOGS:
return {
...state, // a copy of state
};
case DELETE_BLOGS:
return {
...state,
blogs: state.filter((blog) => blog.id !== action.id),
};
default:
return state; // original state
}
};
export default blogReducer;
COMPONENT
import React, { Component } from "react";
import { connect } from "react-redux";
import { deleteBlogs } from "../actions/blogActions";
class AllBlogs extends Component {
removeBlogs = (id) => {
console.log("removeBlogs function is running with id", id);
this.props.deleteBlogs(id); // delete action
};
render() {
return (
<div>
{this.props.blogs.map((blog) => (
<div key={blog.id}>
<h3>{blog.title}</h3>
<p>{blog.content}</p>
<button onClick={() => this.removeBlogs(blog.id)}>delete</button>
<hr />
</div>
))}
</div>
);
}
}
const mapStateToProps = (state) => ({
blogs: state.blogs,
});
export default connect(mapStateToProps, { deleteBlogs })(AllBlogs);
ISSUE
You were sending action key type and receiving as action.types in reducer
SOLUTION
const blogReducer = (state = initialState, action) => {
switch (action.type) { // change `types` to `type`
case DELETE_BLOGS:
return {
...state,
blogs: state.blogs.filter((blog) => blog.id !== action.id)
};
default:
return state; // original state
}
};
The state of the redux store is changing as it should but cannot get the object.map function to re-render the new state. Getting the following error: "TypeError: Cannot read property 'map' of undefined"
Confirmed that data in actions.js is correct, confirmed that data in reducer.js is correct, confirmed state change in state.PrepInfos is correct.
Form:
class PrepInfos extends Component {
render(){
const{ PrepInfos } = this.props;
return(
<Form>
{PrepInfos.map(prepInfo => <PrepInfo key={prepInfo.id} id={prepInfo.id} type={prepInfo.type} quantity={prepInfo.quantity} description={prepInfo.description} />)}
</Form>
);
}
}
const mapStateToProps = state => ({
PrepInfos: state.recipeForm.PrepInfos.PrepInfos,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(PrepInfos);
Actions:
export const H_CHANGE = 'H_CHANGE';
export function hChange(event) {
const form = ({
value: event.target.value,
name: event.target.name,
id: event.target.id,
});
return ({
type: 'H_CHANGE',
data: form,
});
}
Reducer:
import { H_CHANGE } from './PrepInfo/actions';
const initialState = {
PrepInfos: [{id:0, type:"makes", quantity:30, description:"slices"}, {id:1, type:"chill", quantity:15, description:"minutes"}],
};
export default function(state = initialState, action){
const { type, data } = action;
switch(type) {
case H_CHANGE:
return state.PrepInfos.map(prepInfo => {
if (prepInfo.id == data.id) {
return {...prepInfo, [data.name]: data.value}
};
return prepInfo;
});
default:
return state;
}
}
Corrected Reducer:
return Object.assign({}, state, {
PrepInfos: state.PrepInfos.map(prepInfo => {
if (prepInfo.id == data.id) {
return {...prepInfo, [data.name]: data.value}
};
return Object.assign({}, prepInfo, {});
})
})
Expecting to re-render the new state, instead getting TypeError: Cannot read property 'map' of undefined
The bug is caused by mutating state in the reducer
// this is mutating the PrepInfos property in state
return state.PrepInfos.map(prepInfo => {
if (prepInfo.id == data.id) {
return {...prepInfo, [data.name]: data.value}
};
return prepInfo;
});
// this is creating and returning a new obj for state and the PrepInfos key in state
return {
...state,
PrepInfos: state.PrepInfos.map(prepInfo => {
if (prepInfo.id == data.id) {
return {...prepInfo, [data.name]: data.value}
};
return prepInfo;
}
I have this configuration when using react-redux connect().
const mapStateToProps = state => ({
...state
});
const mapDispatchToProps = dispatch => ({
addMessage: msg => dispatch(addMessage(msg))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(RoomChat);
When using this.props.chats I am getting "TypeError: Cannot read property 'map' of undefined".
This is the state, an array of objects that have the fields of 'username' and 'content':
chats = [{ content: '', username: '' }]
It is define in the RoomChat.js like this:
this.state = {
chats: []
};
This is the store.js where the redux store is defined:
import { createStore } from 'redux';
import MessageReducer from './reducers/MessageReducer';
function configureStore(chats = [{ content: '', username: '' }]) {
return createStore(MessageReducer, chats);
}
export default configureStore;
Reducer:
export default (state, action) => {
switch (action.type) {
case 'addMessage':
return {
content: action.text,
username: 'R'
};
default:
return state;
}
};
action:
export function addMessage(text) {
return { type: 'addMessage', text };
}
What went wrong here ?, I have tried multiple configurations without success so far
In your mapStateToProps function, you need to do this...
const mapStateToProps = state => ({
chats: state
});
This is because you're creating your store with the chats array when you do createStore(MessageReducer, chats).
So the state is automatically chats
UPDATE
In addition to #smoak's comment, you need to update your reducer like this
export default (state, action) => {
switch (action.type) {
case 'addMessage':
return [
...state,
{
content: action.text,
username: 'R'
}
];
default:
return state;
}
};
Mabye try this
const mapStateToProps = state => {return {chats: state.chats}};
or this
const mapStateToProps = state => { return {...state}};
You need to return object in mapStateToProps and mapDistpachToProps.
After combining two reducers together (EditButton and TodoApp), my app everytime start crash. Before it, when I just use only one reducer TodoApp I did not have any problem with reducers. But now I cannot figure out what is wrong, because every time I get the error in map function of component below . Error "TypeError: Cannot read property 'map' of undefined".
So, what is I forgot? Also I cannot get the state in nested components or containers of App. It's strange too, but in App I can do that by console.log() for example.
/* REDUCERS */
import { combineReducers } from 'redux'
import { ADD_TODO, EDIT_TODO, DELETE_TODO, FILTER_TODO_UP, FILTER_TODO_DOWN } from '../Variables/Variables'
const initialState = {
todos: []
}
function EditButton(state, action) {
if (typeof state === 'undefined') {
return 'Edit';
}
switch (action.type) {
case EDIT_TODO:
return state = "Edit" ? "Done" : "Edit"
default:
return state
}
}
function TodoApp(state, action) {
if (typeof state === 'undefined') {
return initialState;
}
switch (action.type) {
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
id: action.id,
text: action.text,
done: action.done
}
]
});
case EDIT_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
id: action.id,
text: action.text,
done: action.done
}
]
});
case DELETE_TODO:
return Object.assign({}, {
todos: state.todos.filter(todos => todos.id !== parseInt(action.id))
});
case FILTER_TODO_UP:
return Object.assign({}, {
todos: [
...state.todos.sort((a, b) => b.id - a.id)
]
});
case FILTER_TODO_DOWN:
return Object.assign({}, {
todos: [
...state.todos.sort((a, b) => a.id - b.id)
]
});
default:
return state;
}
}
export default combineReducers({TodoApp, EditButton})
/* APP */
import React, { Fragment } from 'react';
import TodoFormAdd from '../Containers/TodoFormAdd';
import TodoListAdd from '../Containers/TodoListAdd';
import TodoFormFilterAdd from '../Containers/TodoFormFilterAdd';
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return(
<Fragment>
// console.log(this.props.state.getState()) - work!
<TodoFormAdd />
<TodoListAdd store={this.props.store} />
<TodoFormFilterAdd />
</Fragment>
);
}
}
export default App;
/* CONTAINER */
import { connect } from 'react-redux';
import TodoList from '../Components/TodoList/TodoList';
import { DeleteTodo } from '../Actions/AddTodo'
// console.log(this.props.state.getState()) - does not work!
const mapStateToProps = state => ({
todos: state.todos
});
const mapDispatchToProps = dispatch => ({
todoFormDelete: todo => dispatch(DeleteTodo(todo))
});
export default connect(
mapStateToProps,
mapDispatchToProps)(TodoList)
/* COMPONENT */
import React from 'react';
import TodoIteam from '../TodoIteam/TodoIteam'
class TodoList extends React.Component {
handleDelete = (e) => {
let target = e.target;
let closestDelete = target.closest('span');
let closestEdit = target.closest('button');
if (closestDelete) {
let index = closestDelete.parentNode.getAttribute('index');
this.props.todoFormDelete(index);
} else {
return
}
}
render(props) {
// console.log(this.props.state.getState()) - does not work!
return (
<ul onClick={this.handleDelete}>{this.props.todos.map((iteam, index) =>
// this where I get an error
<TodoIteam key={index} index={iteam.id} {...iteam} />
)}
</ul>
);
}
}
export default TodoList;
As you are using ES6 property shorthand notation in combineReducers :
combineReducers({TodoApp, EditButton})
This is equivalent to writing combineReducers({ TodoApp: TodoApp, EditButton: EditButton })
But inside your CONTAINER you are accessing state.todos there is nothing called todos coming from state instead its TodoApp and Hence you get error in your .map():
this.props.todos.map((iteam, index) {}
EDIT :
As you are returning an object containing an array from your reducers called todos so to access correct state you need to use reducer Name followed by an array name you are returning which would be TodoApp.todos
So inside your Container you need to access correct reducer
const mapStateToProps = state => ({
todos: state.TodoApp.todos // Notice TodoApp is used instead of todos
});
You can read more about combineReducers on Redux Documentation
I have this error:
TypeError: Cannot read property 'map' of undefined
And don't know how can this.props.movies be undefined while I have initialState declared as [] and then it must straightly go to render?
So there are files:
Reducer.js
import * as types from "./ActionTypes.js";
const initialState = { movies: [] };
export const reducer = (state = initialState, action) => {
console.log(state.movies);
switch (action.type) {
case types.GET_MOVIES:
return {
...state,
movies: action.value
};
default:
return state;
}
};
Movies.js
import React, { Component } from "react";
import { connect } from "react-redux";
import MovieItem from "./MovieItem";
const mapStateToProps = state => ({
movies: state.movies
});
class Movies extends Component {
render() {
let movieItems = this.props.movies.map(movie => {
return <MovieItem movie={movie} />;
});
return <div className="Movies">{movieItems}</div>;
}
}
export default connect(mapStateToProps, null)(Movies);
And even if I put if-statement like
if (this.props.movies){
...
}else return 1;
it never rerenders
It should be:
const mapStateToProps = state => ({
movies: state.reducer.movies
});
Because your initial state is a object:
initialState = { movies: [] };
And you are using combineReducers like this:
const rootReducer = combineReducers({ reducer: reducer });
Now all the initialState of that reducer will be accessible by state.reducer.propertyName.
Note: Instead of using reducer, better to use some intuitive name like moviesReducer. Later in future if you will add more reducers then it will help to identify the the data.