Update redux state in two places with one action - reactjs

I am building a webshop and I´ve run in to some problems. When I click on the buy button I want three things to happen:
Add the product to the redux state (shoppingcartReducer)
Update the sum and quantity in the redux state (shoppingcartReducer)
Render out the sum and quantity in my shoppingCart.js
component
The problem is in the shoppingcartReducer. I dont know how to update the state in two different paths on one action.
return state
.setIn(['products', action.articleNr], addArticle(action, state))
Here I am in the 'products/articleNr' path and manipulating data, but I also wants to update the sum and quantity in './'.
So my alternatives that I thought of are maybe to have some sort of middleware thing in the shoppingcartActions but I don´t really know if this is right, nor how to do it!
I appreciate all the help, thanks!
shoppingcartReducer:
import Immutable from 'seamless-immutable'
import { addToShoppingcart, createShoppingCart } from '../actions/shoppingcartActions'
const ADD_TO_SHOPPINGCART = 'ADD_TO_SHOPPINGCART'
const CREATE_SHOPPING_CART = 'CREATE_SHOPPING_CART'
const initialState = Immutable({
sum: 0,
quantity: 0
})
export default function shoppingcartReducer(state = initialState, action) {
switch (action.type) {
case ADD_TO_SHOPPINGCART:
return state
.setIn(['products', action.articleNr], addArticle(action, state))
case CREATE_SHOPPING_CART:
return state
.set(action.id, createCart(action))
}
return state
}
function addArticle(action, state) {
return {
product: action.product
}
}
function createCart(action) {
return {
id: action.id,
}
}
shoppingcartActions:
let nextTodoId = 0
const ADD_TO_SHOPPINGCART = 'ADD_TO_SHOPPINGCART'
const CREATE_SHOPPING_CART = 'CREATE_SHOPPING_CART'
export function addToShoppingcart(product) {
return {
type: ADD_TO_SHOPPINGCART,
articleNr: product.articleNr,
product: product,
}
}
export function createShoppingCart() {
return {
type: CREATE_SHOPPING_CART,
id: 'productNr:'+nextTodoId++,
}
}
ShoppingCart.js:
import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as shoppingcartActions from '../../actions/shoppingcartActions'
class ShoppingCart extends Component {
componentWillMount() {
this.state = {
shoppingcartReducer: []
}
}
componentWillReceiveProps(nextProps) {
this.setState({
shoppingcartReducer: nextProps.shoppingcartReducer ? nextProps.shoppingcartReducer : ''
})
}
render() {
const { shoppingcartReducer } = this.props
const { sum, quantity } = shoppingcartReducer
return (
<div className="shoppingCart">
<ul>
<li>Summa: {sum} :-</li>
<li>Antal varor: {quantity}</li>
</ul>
</div>
)
}
}
function mapStateToProps(state) {
return {
shoppingcartReducer: state.shoppingcartReducer
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(shoppingcartActions, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(ShoppingCart)

This worked!
export function addProduct(product) {
return {
type: ADD_PRODUCT,
articleNr: product.articleNr,
product: product,
}
}
export function addSummary(product) {
return {
type: ADD_SUMMARY,
product: product
}
}
export function addToShoppingcart(product) {
return (dispatch) => {
dispatch( addProduct(product))
dispatch( addSummary(product))
}
}

Related

dispatching actions won't trigger render when using combinedReducers

When I don't use combineReducers:
const store = createStore<StoreState,any,any,any>(pointReducer, {
points: 1,
languageName: 'Points',
});
function tick() {
store.dispatch(gameTick());
requestAnimationFrame(tick)
}
tick();
everything works and my component updates. However when I do:
const reducers = combineReducers({pointReducer}) as any;
const store = createStore<StoreState,any,any,any>(reducers, {
points: 1,
languageName: 'Points',
});
The store does update (checked by console logging) however the component doesn't render the change and I have no idea why!
The reducer:
export function pointReducer(state: StoreState, action: EnthusiasmAction): StoreState {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return { ...state, points: state.points + 1 };
case DECREMENT_ENTHUSIASM:
return { ...state, points: Math.max(1, state.points - 1) };
case GAME_TICK:
return { ...state, points: state.points + 1 };
default:
return state;
}
}
and component:
export interface Props {
name: string;
points: number;
onIncrement: () => void;
onDecrement: () => void;
}
class Points extends React.Component<Props, object> {
constructor(props: Props) {
super(props);
}
render() {
const { name, points, onIncrement, onDecrement } = this.props;
return (
<div className="hello">
<div className="greeting">
Hello {name + points}
</div>
<button onClick={onDecrement}>-</button>
<button onClick={onIncrement}>+</button>
</div>
);
}
}
export default Points;
The container:
export function mapStateToProps({ points, languageName }: StoreState) {
return {
points: points,
name: languageName,
}
}
export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
return {
onIncrement: () => dispatch(actions.incrementEnthusiasm()),
onDecrement: () => dispatch(actions.decrementEnthusiasm())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Points);
Store state:
export interface StoreState {
languageName: string;
points: number;
food: number;
wood: number;
}
When making the suggested changes (changing the reducer and combinereducers I get a new error:
my reducer now looks like:
export function pointReducer(state: 1, action: EnthusiasmAction) {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return state + 1;
case DECREMENT_ENTHUSIASM:
return Math.max(1, state - 1);
case GAME_TICK:
return state + 1;
default:
return state;
}
}
The problem is likely in how you're using combineReducers, vs how you're writing your mapState function.
I'm going to guess that your mapState function looks like:
const mapState = (state) => {
return {
points : state.points
}
}
This works okay when you use your pointsReducer by itself, because your state has state.points.
However, when you use combineReducers the way you are, you're creating two problems for yourself:
You're naming the state field state.pointsReducer, not state.points
Your pointsReducer is further nesting the data as points
So, the actual data you want is at state.pointsReducer.points, when the component is expecting it at state.points.
To fix this, you should change how you're calling combineReducers, and change the pointsReducer definition to just track the data without nesting:
export function pointReducer(state: 1, action: EnthusiasmAction): StoreState {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return state + 1
case DECREMENT_ENTHUSIASM:
return Math.max(1, state - 1);
case GAME_TICK:
return state.points + 1;
default:
return state;
}
}
// later
const rootReducer = combineReducers({
points : pointsReducer,
languageName : languageNameReducer,
});
See the Redux docs page on "Using combineReducers" for more details

State doesnt get reset on route change - React-redux-router

Hi I am creating a Server side react app. I have multiple routes using the same components and reducers. One specific reducer is an ItemsPerPage dropdown. I am getting the value from the reducer and passing it as a payload to a post request to a database to fetch that many results.
In one page, I get 50 results, and when I navigate to the other page using the same reducer, the state value should be 10 but rather its 50. How do I reset the state when going to another page?
I am using { LOCATION_CHANGE } from 'react-router-redux'
routerReducer.js:
import { LOCATION_CHANGE } from 'react-router-redux';
const initialState = {
location: '',
};
export const routeReducer = (state = initialState, action) => {
switch (action.type) {
case LOCATION_CHANGE:
return {
...state,
...action.payload,
};
default:
return state;
}
};
ItemsPerPageDropdown:
import React, {PropTypes, Component} from 'react';
import _ from 'lodash';
import { connect } from 'react-redux';
import { changeItemsPerPage } from '../../actions/index';
class ItemsPerPage extends Component {
handleChange = (event) => {
this.props.changeItemsPerPage(event.target.value)
};
render() {
const itemsPerPage = [10, 20, 50];
return (
<div className={'table-item-count-container'}>
<label className={'filter-label items-by-page-label'}>Items Per Page:</label>
<select id="items-per-paghe"
className="form-control items-by-page-select"
onChange={this.handleChange}
>
{_.map(itemsPerPage, (item, index) => <option key={index}>{item}</option>)}
</select>
</div>
)
}
}
export default connect(null, {changeItemsPerPage})(ItemsPerPage);
ItemsPerPageReducer:
import * as ACTION_TYPES from '../consts/action_types';
const initialState = {
items: 10,
};
export const itemsPerPageReducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPES.CHANGE_ITEMS_PER_PAGE:
return {
...state,
items: action.data,
};
default:
return state;
}
};
Main page using this component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom'
import { withRouter } from 'react-router'
import { bindActionCreators } from 'redux';
import _ from 'lodash';
import Moment from 'react-moment';
import Header from '../../components/Header/Header';
import DataTable from '../../components/DataTable/DataTable';
import InstructionList from '../../components/InstructionList/InstructionList';
import { getData, postData } from '../../actions';
import { columns } from './Report1Columns';
import * as instructions from '../../consts/instructionText';
class Report1 extends Component {
params = {
userId: this.props.corpId,
filteredAppID: '',
envClassification: '',
Status: '',
startRow: 0 + this.props.activePage, //1
numRows: this.props.itemsPerPage,
sortCol: 'application_name',
sortDir: 'asc',
};
loadPage = () => {
if(this.props.postData) {
this.props.postData('https://localhost:3001/reports/report1/', this.params);
}
};
componentDidMount = () => {
this.props.postData('https://localhost:3001/reports/report1/', this.params);
};
componentWillReceiveProps = (nextProps) => {
if (nextProps.itemsPerPage !== this.props.itemsPerPage) {
this.params.numRows = nextProps.itemsPerPage;
this.loadPage();
}
if(nextProps.activePage !== this.props.activePage) {
this.params.startRow = ((nextProps.activePage - 1) * this.props.itemsPerPage) +1;
this.loadPage();
}
if(nextProps.searchTerm !== this.props.searchTerm) {
this.params.filteredAppID = nextProps.searchTerm;
this.loadPage();
}
if(nextProps.envClassification !== this.props.envClassification) {
this.params.envClassification = nextProps.envClassification === 'All' ? '' : nextProps.envClassification;
this.loadPage();
}
if(nextProps.watchtowerStatus !== this.props.Status) {
this.params.watchtowerStatus= nextProps.watchtowerStatus=== 'Manage & Analyze' ? '' : nextProps.watchtowerStatus;
this.loadPage();
}
};
render() {
return (
<div>
<Header title={ 'Report 1' } />
<InstructionList instructions={ instructions.Report1 } />
{this.props.data &&
<DataTable
keyField={ 'Report1' }
columns={ columns }
paginatedData={ this.props.data }
totalRows={ this.props.totalRows }
placeholder={ 'ID/NAME' }
showStatus={true}
/>}
</div>
);
}
}
const mapStateToProps = state => ({
/**
* The date to be passed to the table
*/
data: _.get(state.isFetchingPost, 'data.rows'),
/**
* Total Rows count of data
*/
totalRows: state.isFetchingPost.data.total_rows,
/**
* Items Per Page
*/
itemsPerPage: state.itemsPerPageReducer.items,
/**
* Item which is searched
*/
searchTerm: state.searchReducer.searchTerm,
/**
* The current active page for pagination
*/
activePage: state.changeActivePageReducer.activePage,
/**
* The value of the dropdown selected in Environment Classification
*/
envClassification: state.envClassificationReducer.envClassification,
/**
* The value of the dropdown selected in Status
*/
watchtowerStatus: state.watchTowerStatusReducer.watchTowerStatus
});
const mapDispatchToProps = dispatch => bindActionCreators({ getData, postData }, dispatch);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Report1));
If you can see in the images below, I navigate between the routes and state still remains same instead of taking initial state.
Add a case for LOCATION_CHANGE to your ItemsPerPageReducer to reset the count when the location changes:
import * as ACTION_TYPES from '../consts/action_types';
import { LOCATION_CHANGE } from 'react-router-redux';
const initialState = {
items: 10,
};
export const itemsPerPageReducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPES.CHANGE_ITEMS_PER_PAGE:
return {
...state,
items: action.data,
};
case LOCATION_CHANGE:
return {
...state,
items: 10,
};
default:
return state;
}
}
};
If you only want it to reset on certain location changes, you can check action.payload to test if it is a route you actually want to reset on.

How to re-render child component In this situation?

Problem
If I click a number, the number should increase but not increase.
As you can see, child components are not re-rendered. (If I change the key of the 'li' element to Math.random() it works fine.)
How can I solve this situation?
An example was posted on https://codesandbox.io/s/p5q30rxk47
Thanks for reading.
Source code
The source code is roughly as follows.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Parent from './Parent';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducer from './redux';
const body = document.querySelector('body'),
store = createStore(reducer);
ReactDOM.render(<Provider store={store}><Parent/></Provider>, body);
Parent.js
import React from 'react';
import { connect } from 'react-redux';
import Child from './Child';
class Parent extends React.PureComponent {
get_numbers () {
return this.props.numbers.map((number) => (
<li key={number.id}>
<span>number : </span><br/>
<Child number={number} />
</li>
));
}
render () {
return (
<ul>
{this.get_numbers()}
</ul>
);
}
}
function mapStateToProps(state) {
return { numbers: state.numbers };
}
Parent = connect(mapStateToProps)(Parent);
export default Parent;
Child.js
import React from 'react';
import { connect } from 'react-redux';
import { increase_number } from './redux';
class Child extends React.PureComponent {
render() {
return (
<span onClick={() => this.props.increase_number(this.props.number)}>{this.props.number.value}</span>
);
}
}
function mapDispatchToProps(dispatch) {
return {
increase_number: (number) => dispatch(increase_number({ number }))
};
}
Child = connect(undefined, mapDispatchToProps)(Child);
export default Child;
redux.js
import { createAction, handleActions } from 'redux-actions';
export const increase_number = createAction('increase_number');
const initial_state = {
numbers: [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 }
]
};
export default handleActions({
increase_number: (state, action) => {
// console.log(action.payload.number.value);
action.payload.number.value++;
// console.log(action.payload.number.value);
return { ...state, numbers: [...state.numbers] }
}
}, initial_state);
This is because keys helps you to figure if that element
has changed when it gets compared in the virtual Dom,
so if the id is same for the elements,
ie : 1 for first li, 2 for second li,
the dom will never know since the element updates
based on change in virtual dom , even though the values
are changing
possible solution could be to use id's different from 1, 2, 3 and update the id along with the value so that the Dom is able to figure out the change.
one Possible hack could be
increase_number: (state, action) => {
//console.log(action.payload.number.value);
action.payload.number.value++;
action.payload.number.id--;
//console.log(action.payload.number.value);
return { ...state, numbers: [...state.numbers] }
}
now the key will update everytime with the value but should
not increase since it will be same key of the 2nd li
and the dom will give you an error
so everytime 1 goes to 2 its id will go -- ie 1-1 =0
here is the working Sample
https://codesandbox.io/s/mz6zy5rq28
You need to get a deep copy of numbers array and then increase the value of the number that is passed in payload, like this:
export default handleActions({
increase_number: (state, action) => {
let numberToModify = action.payload.number;
// make a new array with new objects
let newNumbers = state.numbers.map(n => ({ id: n.id, value: n.value }));
// find the one that needs to be modified
let existingNumber = newNumbers.find((number) => (number.id === numberToModify.id));
if (existingNumber) {
existingNumber.value++;
}
return { ...state, numbers: newNumbers };
}
}, initial_state);
Made a working example here.
You need to modify the handleAction as below
export default handleActions({
increase_number: (state, action) => {
action.payload.number.value++
const currentNum = action.payload.number;
let newLst = state.numbers.map((num) => {
return num.id === currentNum.id ? {...action.payload.number} : num
})
return { ...state, numbers: newLst }
}
}, initial_state);
Here is the working sample
https://codesandbox.io/s/4jor55xz5w
Slightly modified you handleActions
export default handleActions({
increase_number: (state, action) => {
action.payload.number.value++;
const currNumber = action.payload.number;
const numbers = state.numbers.map((num) => {
return num.id === currNumber.id ? { ...action.payload.number } : num
});
return { ...state, numbers }
}
}, initial_state);
For increasing the value by one you should make changes in your redux.js file like below:
import { createAction, handleActions } from 'redux-actions';
export const increase_number = createAction('increase_number');
const initial_state = {
numbers: [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 }
]
};
export default handleActions({
increase_number: (state, action) => {
console.log(action.payload.number.value);
// action.payload.numbers.value++;
const numbers = [...state.numbers];
numbers.push({
id: action.payload.number.value++,
value: action.payload.number.value++
})
// console.log(action.payload.number.value);
return { ...state, numbers: numbers }
}
}, initial_state);`

Change in state do not propagate in props

I have the "classic" issue with the React redux about not propagating the change in state into the props when I try to access it in the component.
Here I have read that
99.9% of the time, this is because you are accidentally mutating data, usually in your reducer
Can you tell me what am I doing wrong? Is this the good way how to do the deep copy of the property of the specified object in array?
note: in the reducer in the return statement the state is clearly changed correctly (debugged it)
reducer:
case 'TOGGLE_SELECTED_TAG':
const toggledTagId = action.payload;
const index = findItemById(state.tags, toggledTagId);
const newTags = state.tags.slice(0);
if(index >= 0)
{
newTags[index] = Object.assign(
state.tags[index],
{selected: !state.tags[index].selected});
state.tags = newTags;
}
return Object.assign({}, state);
component:
import React from 'react';
import { Button, FormControl, Table, Modal } from 'react-bootstrap';
import { connect } from 'react-redux';
import axios from 'axios';
import {selectTagAction} from '../../actions/actions'
#connect((store) => {
return {
tags: store.TagsReducer.tags,
}
})
export default class AssignTag extends React.Component {
constructor(props) {
super(props);
this.handleTagClick = this.handleTagClick.bind(this);
}
handleTagClick(element) {
debugger;
this.props.dispatch(selectTagAction(element));
}
render() {
const tags = this.props.tags;
console.log(tags);
const mappedTags = tags.map(tag => {
return (
<div className="col-sm-12" key={tag.id} onClick={() => this.handleTagClick(tag.id)}
style={{background: this.getBackgroundColor(tag.selected)}}>
<span>{tag.name}</span>
</div>
)
})
// code continues
}
}
You are indeed mutating the state. Try this:
case 'TOGGLE_SELECTED_TAG':
const toggledTagId = action.payload;
const index = findItemById(state.tags, toggledTagId);
let newTags = state;
if( index >= 0 )
{
newTags[index] = Object.assign(
{},
state.tags[index],
{ selected: !state.tags[index].selected }
);
//state.tags = newTags; This line essentially mutates the state
return Object.assign( {}, state, { tags: newTags });
}
return state;
Another workaround to avoiding mutation of state is to use the ES6 shorthand in your reducer:
.... return { ...state, tags : newTags };

React/Redux Connect issues with mapStateToProps

I have the following React Component
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import _ from 'lodash';
import Product from './product';
import { openPaymentModal } from '../../../state/modalActions';
import { fetchAccountProducts } from '../../../lib/ecApi';
import { fetchChargifyCallById } from '../../../lib/chargifyApi';
import { filterProductsForUser, prepProducts } from '../../../_helpers';
class Products extends Component {
constructor () {
super();
this.state = {
products: [],
currentProduct: '',
showSuccess: false,
}
}
componentDidMount() {
const { location, user } = this.props;
fetchAccountProducts()
.then(this.addBasicProduct)
.then(this.filterProducts(user));
this.checkChargifyCall(location.query, user);
}
addBasicProduct(products) {
return prepProducts(products);
}
filterProducts(user) {
return products => {
this.setState({products: filterProductsForUser(products, user)});
}
}
checkChargifyCall (query, user) {
if (_.isEmpty(query)) {
const currentProduct = this.determineProduct(user);
this.setState({currentProduct});
return;
}
fetchChargifyCallById(query.call_id).done(data => {
const { product } = data.response.signup;
const { errors } = data.response.meta;
if (query && query.status_code !== '200') {
this.props.dispatch(openPaymentModal(
product.handle,
errors,
));
} else {
this.setState({
currentProduct: product.handle,
showSuccess: true
});
}
});
}
determineProduct(user) {
const subscription = user.chargifySubscriptions[0];
if (subscription && subscription.product) {
return subscription.product.handle;
}
return this.state.currentProduct;
}
render () {
let calloutEl = (
<div className='callout success'>Success!</div>
);
return (
<div className="row medium-up-2 large-up-3 products-row">
{this.state.showSuccess && calloutEl}
{this.state.products.map((object, i) => {
return <div className="column"><Product
price={object.price}
name={object.name}
interval={object.interval}
intervalUnit={object.interval_unit}
isPaid={object.require_credit_card}
description={object.description}
handle={object.handle}
key={i}
currentProduct={this.state.currentProduct} /></div>;
})}
</div>
);
}
}
const mapStateToProps = state => ({user: state.user});
export default connect(mapStateToProps)(Products);
The problem I am having is that if I console.log(this.props.user) in my componentDidMount method, it is the initial state from the reducer vs the fully propagated user state. Any reasons why that might be happening? I'm fairly new to React/Redux, so my apologies for ignorance
Answer: it is the initial state from the reducer.
reason reducer represents a piece of state. and please your a promise middle to handle your data fetching.
The problem I am having is that if I console.log(this.props.user) in my componentDidMount method, it is the initial state from the reducer vs the fully propagated user state. Any reasons why that might be happening? I'm fairly new to React/Redux, so my apologies for ignorance.
connect is a high order component that passes data to your container component. in your case Products component receives state as props from connect
const mapStateToProps = state => ({user: state.user}); //the state you want
export default connect(mapStateToProps)(Products); //user as state to you products component.

Resources