Change in state not being rendered with object.map - reactjs

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;
}

Related

Redux state changes but not reflected in component

I'm trying to implement a shopping cart using redux. here is my cart-reducer:
export const cartReducer = (
state = { cartItems: JSON.parse(localStorage.getItem("cartItems") || "[]")},
action) => {
switch (action.type) {
case ADD_TO_CART:
return { ...state,cartItems: action.payload };
}}
Here is the component where I want to show the updated state value accessing it using props in connect and update the cartItems.length after every state update
class Cartsidebar extends Component {
constructor(props) {
super(props);
this.state = {
grandTotal: '',toggle:true
}
}
handleHide(){
this.setState({ toggle: !this.state.toggle})
}
render() {
const {cartItems}=this.props;
console.log(cartItems);
return (
<div>
{cartItems.length}
</div>
)
}
}
export default connect(
(state) => ({
cartItems: state.cart.cartItems,
}),
{ incrementToCart, decreaseToCart, removeFromCart }
)(Cartsidebar);
States are updating fine and state-difference is also showing in redux-dev-tools on every update of redux state but it is not reflecting in cart component.what am i doing wrong here?Thanks in advance.
EDIT:
this is function that execute on add to cart button onclick event:
handleAddToCart=(p)=>{
const cartItems = store.getState().cart.cartItems;
let alreadyExists = false;
cartItems.forEach((x) => {
if (x.discountPer === p.discountPer) {
alreadyExists = true;
}
});
if (!alreadyExists) {
cartItems.push({ ...p });
}
store.dispatch(addToCart(cartItems));
}
And addToCart action creator looks like this:
export const addToCart = (cartItem) => {
return({
type: ADD_TO_CART,
payload: cartItem,
});
};
Issues
You are mutating the state object. You are accessing a reference to the cart array in state and directly pushing into it.
You aren't leveraging the power of Redux and reducers properly.
code
handleAddToCart = (p) => {
const cartItems = store.getState().cart.cartItems; // cartItems is reference to state
let alreadyExists = false;
cartItems.forEach((x) => {
if (x.discountPer === p.discountPer) {
alreadyExists = true;
}
});
if (!alreadyExists) {
cartItems.push({ ...p }); // mutation!!
}
store.dispatch(addToCart(cartItems));
}
Solution
Pass the item you want to add to the cart in the action and move all the logic to update the cart into your reducer.
UI
handleAddToCart = (p) => {
this.props.addToCart(p);
}
...
export default connect(
(state) => ({
cartItems: state.cart.cartItems,
}),
{ addToCart, incrementToCart, decreaseToCart, removeFromCart }
)(Cartsidebar);
reducer
case ADD_TO_CART:
const { payload } = action;
const found = state.cartItems.find(item => item.discountPer === payload.discountPer);
if (found) {
return state;
}
return {
...state,
cartItems: state.cartItems.concat(payload),
};
What you are doing in handleAddToCart is a big no no, and goes against the pattern that Redux tries to enforce. I made some changes to your logic to make it easier, and updated the reducer. In theory, if you make these changes, it should work.
handleAddToCart:
handleAddToCart = (p) => {
const cartItems = store.getState().cart.cartItems;
for (const item of cartItems) {
if (item.discountPer === p.discountPer) {
return;
}
}
store.dispatch(addToCart({ ...p }));
};
reducer:
export const cartReducer = (
state = { cartItems: JSON.parse(localStorage.getItem("cartItems") || "[]") },
action
) => {
switch (action.type) {
case ADD_TO_CART:
return { ...state, cartItems: [...state.cartItems, action.payload] };
}
};

Difference in the value received in this.props when using mapStateToProps (react-redux) vs store.getState()

I am facing an issue in my code base so I have made a sample code to demonstrate the issue.
link for the codesandbox code
App.js
import React, { Component } from 'react';
import './App.css';
import { connect } from 'react-redux';
import { handleDataInit, handlePageChange, handleDataAdded } from './appDataAction';
import First from './First';
import Second from './Second';
import { reduxStore } from "./store";
class App extends Component {
handleChange = (pageNumber, pageTitle) => {
let data = {
val1: "val1",
val2: "val2",
val3: "val3"
}
this.props.handleDataAdded(data);
console.log("app Data", this.props.appData);
console.log('app data in redux store ', reduxStore.getState().appData);
this.props.handlePageChange({ pageNumber, pageTitle });
}
render() {
return (
<div>
<button onClick={() => this.handleChange(1, "first_page")}>1</button>
<button onClick={() => this.handleChange(2, "second_page")}>2</button>
{
this.props.appData.pageNumber === 1 ?
<First />
:
<Second />
}
</div>
);
}
}
const mapStateToProps = (state) => {
console.log('map state to props state value is ', state);
return ({
appData: state && state.appData
})
}
const mapDispatchToProps = (dispatch) => {
return ({
handleDataInit: (data) => dispatch(handleDataInit(data)),
handlePageChange: (newPage) => dispatch(handlePageChange(newPage)),
handleDataAdded: (data) => dispatch(handleDataAdded(data))
})
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
screenshot for the two console.log
browser console log:
appDataAction.js
export const handleDataInit = (data) => {
return ({
type: "data_init",
payload: data
});
}
export const handlePageChange = (newPage) => {
return ({
type: "page_change",
payload: newPage
});
}
export const handleDataAdded = (data) => {
return ({
type: "data_added",
payload: data
});
}
appDataReducer.js
const initialState = {
pageNumber: 1,
pageTitle: "first_page",
}
export const appDataReducer = (state = initialState, action) => {
switch (action.type) {
case "data_init":
if (Object.keys(state).length > 2) {
return state
}
else {
let newState = Object.assign({}, state, action.payload);
// console.log("new state in init ", newState);
return newState;
}
case "page_change":
// console.log('action.payload', action.payload);
let newState2 = {
...state,
pageNumber: action.payload.pageNumber,
pageTitle: action.payload.pageTitle
}
// console.log('new state is ', newState2);
return newState2;
case "data_added":
let newState3 = Object.assign({}, state, action.payload);
// console.log("new state in data added ", newState3);
return newState3;
default:
return state;
}
}
From react-redux documentation
The first argument to a mapStateToProps function is the entire Redux store state (the same value returned by a call to store.getState()).
can somebody explain why there is difference in the two console's.
I have debugged and found out that after return from reducer mapStateToProps is called and it gets the updated value of state
then why is this.props.appData is not up to date in the handleChange function.
I believe it could be something related to dirty state but if it is proper for getState() in the function it should be for this.props.appData too.

How to I pass an object to react-redux

I'm a little confused on passing an object to the redux store. I have successfully created the store and can add items from the initial state. The function also fires when called
Action:
import { GET_ITEM } from './OrderTypes'
export const getItem = (payload) => {
return {
type: GET_ITEM,
payload: { payload }
}
}
Reducer:
import { GET_ITEM } from './OrderTypes'
const initialState = {
orderList: [],
}
const orderReducer = (state = initialState, action) => {
switch (action.type) {
case GET_ITEM: return {
...state,
orderList: [...state.orderList, action.payload]
}
default: return state
}
}
export default orderReducer
Component:
class TestComponentextends Component {
pushItem = () => {
this.props.getItem({
payload: 'test object'
})
}
render() {
return (
<input type='button' value='test btn' onClick={this.pushItem} />
)
}
}
const mapStateToProps = state => {
return {
orderList: state.orderList
}
}
const mapDispatchToProps = dispatch => {
return {
getItem: () => dispatch(getItem())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TestComponent)
What happens: An empty object is added to the orderList array.
What I want to happen: Store the object in pushItem in the orderList array.
Your mapDispatchToProps doesn't pass the arguments to the action creator (see mapDispatchToProps function arguments - 2nd example):
const mapDispatchToProps = dispatch => ({
getItem: (...args) => dispatch(getItem(...args))
})
Even easier is to let react-redux handle the mapping by using mapDispatchToProps as an object:
const mapDispatchToProps = {
getItem
}

Dispatching action calls incorrect reducer

Summary
In order to learn Redux, I am incorporating some state, actions, reducers, and trying to see how they are used in React Components.
I have set up a test object...
const initialState = {
navigationCount : 0,
someNumber : 500,
someList : ['aa',22,'c5d6','45615'],
};
...and aim to:
increment the navigationCount by 1 when visiting pages
add or subtract from someNumber
push() & pop() elements from someList.
Versions
Currently using gatsby ^2.5.0, react ^16.8.6, and react-redux ^6.0.1.
Code
actions & reducers
import { combineReducers } from 'redux';
import {
PAGE_INCREMENT,
NUMBER_INCREASE,
NUMBER_DECREASE,
LIST_PUSH,
LIST_POP,
} from './actionTypes.js';
// state
const initialState = {
navigationCount : 0,
someNumber : 500,
someList : ['aa',22,'c5d6','45615'],
};
// action creators returning actions
export const pageIncrementer = navigationCount => {
return {
type: PAGE_INCREMENT,
navigationCount,
};
};
export const numberAdder = numberToAdd => {
return {
type: NUMBER_INCREASE,
numberToAdd,
};
};
export const numberMinuser = numberToMinus => {
return {
type: NUMBER_DECREASE,
numberToMinus,
};
};
export const listPusher = itemToAdd => {
return {
type: LIST_PUSH,
itemToAdd,
}
};
export const listPopper = () => {
return {
type: LIST_POP,
}
};
// reducers
const pageIncrementReducer = (state = initialState, action) => {
switch (action.type) {
case PAGE_INCREMENT:
return Object.assign({}, ...state, {
navigationCount: action.navigationCount+1
});
default:
return state.navigationCount;
}
};
const numberChanger = (state = initialState, action) => {
switch (action.type) {
case NUMBER_INCREASE:
return Object.assign({}, ...state, {
someNumber: state.someNumber+action.numberToAdd,
});
case NUMBER_DECREASE:
return Object.assign({}, ...state, {
someNumber: state.someNumber-action.numberToMinus,
});
default:
return state.someNumber;
};
};
const listChanger = (state = initialState, action) => {
switch (action.type) {
case LIST_POP:
return Object.assign({}, ...state, {
someList: state.someList.pop(),
});
case LIST_PUSH:
return Object.assign({}, ...state, {
someList: state.someList.push(action.itemToAdd),
});
default:
return state.someList;
}
}
// store
const rootReducer = combineReducers({
pageIncrementReducer,
numberChanger,
listChanger,
});
export default rootReducer;
React Component
import React from 'react';
import Layout from '../components/common/Layout.jsx';
import LandingBanner from '../components/landing/LandingBanner.jsx';
import LandingNavgrid from '../components/landing/LandingNavgrid.jsx';
import LandingApp from '../components/landing/LandingApp.jsx';
import { connect } from 'react-redux';
import {
PAGE_INCREMENT,
NUMBER_INCREASE,
NUMBER_DECREASE,
LIST_PUSH,
LIST_POP,
} from '../state/actionTypes';
class LandingPage extends React.Component {
constructor(props){
super(props);
this.state = {
appliedNum: 2000,
};
}
componentDidMount(){
// this.props.pageIncrement(); // => numberChanger returned undefined
// this.props.numberIncrease(4444); // => pageIncrementReducer returned undefined
// this.props.numberDecrease(4444); // => pageIncrementReducer returned undefined
// this.props.listPush(4444); // => pageIncrementReducer returned undefined
this.props.listPop();
}
render(){
return (
<Layout>
<LandingBanner/>
<LandingNavgrid/>
<LandingApp/>
</Layout>
)
}
}
const filterNumbers = (list=[]) => {
console.log('filterNumbers list: ', list);
return list.filter(listElement => !!Number(listElement));
};
const mapStateToProps = (state, ownProps) => {
return {
someNumber: state.someNumber,
someList: filterNumbers(state.someList),
navigationCount: state.navigationCount,
};
};
const mapDispatchToProps = (dispatch) => {
return {
pageIncrement: () => dispatch({ type: PAGE_INCREMENT }),
numberIncrease: () => dispatch({ type: NUMBER_INCREASE }),
numberDecrease: () => dispatch({ type: NUMBER_DECREASE }),
listPush: () => dispatch({ type: LIST_PUSH }),
listPop: () => dispatch({ type: LIST_POP }),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(LandingPage);
Errors
redux.js:449 Uncaught Error: Given action "LIST_POP", reducer
"pageIncrementReducer" returned undefined. To ignore an action, you
must explicitly return the previous state. If you want this reducer to
hold no value, you can return null instead of undefined.
first of all, you always need to return state on the default switch case.
default:
return state;

State is undefined in mapStateToProps

I've been trying to retrieve the new state from my vitaminReducer() reducer function, and connect it through mapStateToProps. But when I console.log the state, I get back "the state is {vitamin: undefined}".
This is the Vitamins component where I'm calling mapStateToProps()
(Vitamins.js)
componentDidMount() {
this.props.fetchVitamins();
}
function mapStateToProps(state) {
return {
vitamin: state,
}
};
console.log('the state is', mapStateToProps());
export default connect(mapStateToProps, { fetchVitamins })(Vitamins);
(reducers.js)
function vitaminReducer(state = [], action) {
switch(action.type) {
case FETCH_VITAMINS_SUCCESS:
return [
...state,
action.payload.vitamins
];
default:
return state;
}
}
const reducers = combineReducers({
vitamin: vitaminReducer,
});
I have the data coming through an Express server. I've console logged "vitamins" here and I get the data back, so I know that's not the issue.
(actions.js)
export function fetchVitamins() {
return dispatch => {
return fetch("/users")
.then(handleErrors)
.then(res => res.json())
.then(micros => {
dispatch(fetchVitaminsSuccess(micros));
const vitamins = micros.vitamins;
}
)};
};
export const FETCH_VITAMINS_SUCCESS = 'FETCH_VITAMINS_SUCCESS';
export const fetchVitaminsSuccess = vitamins => ({
type: FETCH_VITAMINS_SUCCESS,
payload: vitamins
});
If I do: "return { vitamin: state.vitamin, }" instead of "return { vitamin: state, }", I get back "TypeError: Cannot read property 'vitamin' of undefined". But that's what I called vitaminReducer in my combineReducers() function at the bottom of reducers.js, so I thought that was the right way to do it.
Thank you everyone for your input! I was able to get it working.
I ditched the mapStateToProps() and instead did this
(Vitamins.js)
componentDidMount() {
this.props.fetchVitamins();
}
renderData() {
const { vitamins } = this.props.vitamins;
return vitamins.map((micro, index) => {
return (
<option value={micro.value} key={index}>{micro.name}</option>
)
})
}
export default connect(
state => ({
vitamins: state.vitamins
}),
{
fetchVitamins
},
)(Vitamins);
I set the dispatch action inside of the fetchVitamins() function
(actions.js)
export function fetchVitamins() {
return dispatch => {
return fetch("/users")
.then(handleErrors)
.then(res => res.json())
.then(micros => {
dispatch({
type: "RECEIVE_VITAMINS",
payload: micros.vitamins
});
}
)};
};
export const RECEIVE_VITAMINS = 'RECEIVE_VITAMINS';
In reducers I set the initialState to the vitamins array, and passed the new state of micros.vitamins from my RECEIVE_VITAMINS action
(reducers.js)
const initialState = {
vitamins: [],
}
function vitaminReducer(state = initialState, action) {
switch(action.type) {
case RECEIVE_VITAMINS:
return {
...state,
vitamins: action.payload
};
default:
return state;
}
}
const reducers = combineReducers({
vitamins: vitaminReducer,
});
Thanks everyone for your help! Let me know if you have any other suggestions :D

Resources