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 };
Related
This is the response from redux store :
{
"newsletter": true,
"orderConfirmation": true,
"shippingInformation": true,
"orderEnquiryConfirmation": true,
}
This is the jsx file, where am trying to set state. The idea is setting the state from the response and add an onChange handle to each checkboxes.
But currently am receiving a correct response but I tried to set state in didUpdate, DidMount but no luck. I want to know the correct place to set state on initial render of the component.
import React from 'react';
import Component from '../../assets/js/app/component.jsx';
import { connect } from 'react-redux';
import * as actionCreators from '../../assets/js/app/some/actions';
import { bindActionCreators } from 'redux';
import Checkbox from '../checkbox/checkbox.jsx';
const mapStateToProps = (state, ownProps) => {
return {
...state.emailSubscriptions
}
}
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators(actionCreators, dispatch)
}
}
#connect(mapStateToProps, mapDispatchToProps)
class EmailSubscriptions extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
this.props.actions.getEmailSubscriptions();
this.setState({ // Not setting state
notifications: [
newsletter = this.props.newsletter,
orderConfirmation = this.props.orderConfirmation,
shippingInformation = this.props.shippingInformation,
orderEnquiryConfirmation = this.props.orderEnquiryConfirmation
]
})
}
render() {
return (
<div>
Here I want to use loop through state to create checkboxes
{this.state.notifications&& this.state.notifications.map((item, index) => {
const checkboxProps = {
id: 'subscription' + index,
name: 'subscription',
checked: item.subscription ? true : false,
onChange: (e)=>{ return this.onChange(e, index)},
};
return <div key={index}>
<Checkbox {...checkboxProps} />
</div>
</div>
)
}
}
export default EmailSubscriptions;
I hope getEmailSubscriptions is an async action, so your setState won't update the state as you intended. add componentDidUpdate hook in your class component and your setState statement within an if statement that has an expression checking your props current and prev value.
You can do something like this.
componentDidMount() {
this.props.actions.getEmailSubscriptions();
}
componentDidUpdate(prevProps, prevState, snapshot){
if(this.props.<prop_name> != prevProps.<prop_name>){
this.setState({
notifications: [
newsletter = this.props.newsletter,
orderConfirmation = this.props.orderConfirmation,
shippingInformation = this.props.shippingInformation,
orderEnquiryConfirmation = this.props.orderEnquiryConfirmation
]
})
}
}
I'm building a small app using React, semantic-ui-react, redux-subspace.
I have many different tables and when the user clicks on one of the cells, the value supposed to come out on the console but the result is undefined when it clicked. I'm trying to reuse reducer. Same action with different instances.
I appreciate any comments that guide me to right direction.
PartA.js
This component renders Tables and wrapped with <SubspaceProvider>.
<Segment inverted color='black'>
<h1>Age </h1>
{ this.state.toggle ?
<SubspaceProvider mapState={state => state.withSpouseAge} namespace="withSpouseAge">
<TableForm
headers={spouse_ageHeaders}
rows={spouse_ageData}
namespace={'withSpouseAge'}
/>
</SubspaceProvider> :
<SubspaceProvider mapState={state => state.withoutSpouseAge} namespace="withoutSpouseAge">
<TableForm
headers={withoutSpouse_ageHeader}
rows={withoutSpouse_ageData}
namespace={'withoutSpouseAge'}
/>
</SubspaceProvider> }
TableForm.js
This component return Table with the Data and this is where I want to implement onClick method.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Table } from 'semantic-ui-react';
import { select } from '../actions';
const shortid = require('shortid');
class TableForm extends Component {
constructor(props){
super(props);
this.state = {
activeIndex: 0,
}
this.handleOnClick = this.handleOnClick.bind(this);
this.isCellActive = this.isCellActive.bind(this);
};
isCellActive(index) {
this.setState({ activeIndex: index });
}
handleOnClick(index, point) {
this.isCellActive(index);
this.props.onSelect(point);
};
tableForm = ({ headers, rows }) => {
const customRenderRow = ({ factor, point, point2 }, index ) => ({
key: shortid.generate(),
cells: [
<Table.Cell content={factor || 'N/A'} />,
<Table.Cell
content={point}
active={index === this.state.activeIndex}
textAlign={'center'}
selectable
onClick={() => this.handleOnClick(index, point)}
/>,
<Table.Cell
content={point2}
textAlign={'center'}
selectable
/>
],
});
return (
<Table
size='large'
padded
striped
celled
verticalAlign={'middle'}
headerRow={this.props.headers}
renderBodyRow={customRenderRow}
tableData={this.props.rows}
/>
)
};
render() {
console.log(this.props.withSpouseAgePoint);
const { headers, rows } = this.props;
return (
<div>
{this.tableForm(headers, rows)}
</div>
);
}
};
const mapDispatchToProps = (dispatch) => {
return {
onSelect: (point) => {dispatch(select(point))},
}
}
const mapStateToProps = state => {
return {
withSpouseAgePoint: state.withSpouseAge,
withSpouseLoePoint: state.withSpouseLoe,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TableForm);
Action
import {
SELECT,
} from './types';
export const select = (points) => ({
type: 'SELECT',
points,
});
Reducer.js
import { SELECT } from '../actions/types';
const INITIAL_STATE = {
point: 0,
};
const selectionReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'SELECT':
return { ...state, point: state.point + action.points };
default:
return state;
}
};
export default selectionReducer;
Reducer index.js
import { createStore, combineReducers } from 'redux';
import { subspace, namespaced } from 'redux-subspace';
import selectionReducer from './selectionReducer';
import toggleReducer from './toggleReducer';
const reducers = combineReducers({
withSpouseAge: namespaced('withSpouseAge')(selectionReducer),
withSpouseLoe: namespaced('withSpouseLoe')(selectionReducer),
withSpouseOlp: namespaced('withSpouseOlp')(selectionReducer),
withSpouseOlp2: namespaced('withSpouseOlp2')(selectionReducer),
withSpouseExp: namespaced('withSpouseExp')(selectionReducer),
withoutSpouseAge: namespaced('withoutSpouseAge')(selectionReducer),
withoutSpouseLoe: namespaced('withoutSpouseLoe')(selectionReducer),
withoutSpouseOlp: namespaced('withoutSpouseOlp')(selectionReducer),
withoutSpouseOlp2: namespaced('withoutSpouseOlp2')(selectionReducer),
withoutSpouseExp: namespaced('withoutSpouseExp')(selectionReducer),
toggle: toggleReducer,
});
Update
I added below TableForm component
const mapDispatchToProps = (dispatch) => {
return {
onSelect: (point) => {dispatch(select(point))},
}
}
const mapStateToProps = state => {
return {
withSpouseAgePoint: state.withSpouseAge,
withSpouseLoePoint: state.withSpouseLoe,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TableForm);
implement this.props.onSelect(point) on handleOnClick. It still shows me the same result undefined. I checked store states by getState(). consloe.log. I think my implementation of redux-subspace is wrong. I uploaded whole TableForm component and also updated reducer. Please help me out!
update 2
I replaced mapStateToProps and it worked like a magic. Thank you again #JustinTRoss.
but there is another problem, all the states are coming out with the same value when I clicked on the cell.
. my plan is each state has their own value stored.
const mapStateToProps = state => {
return {
withSpouseAgePoint: state,
withoutSpouseAge: state,
}
}
You have already namespaced your component to withSpouseAge and mapped state to state.withSpouseAge in your SubspaceProvider. Thus, you're calling the equivalent of state.withSpouseAge.withSpouseAge (undefined).
Another potential issue is the signature with which you are calling connect. From the snippet you provided, there's no way to be sure of the value of 'select'. Typically, connect is called with 2 functions, often named mapStateToProps and mapDispatchToProps. You are calling connect with a function and an object. Here's an example from http://www.sohamkamani.com/blog/2017/03/31/react-redux-connect-explained/#connect :
import {connect} from 'react-redux'
const TodoItem = ({todo, destroyTodo}) => {
return (
<div>
{todo.text}
<span onClick={destroyTodo}> x </span>
</div>
)
}
const mapStateToProps = state => {
return {
todo : state.todos[0]
}
}
const mapDispatchToProps = dispatch => {
return {
destroyTodo : () => dispatch({
type : 'DESTROY_TODO'
})
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoItem)
Additionally, there's one other issue, although it isn't affecting you yet: You're calling this.tableForm with 2 arguments (headers and rows), while you defined the this.tableForm function to take a single argument and destructure out 'headers' and 'rows' properties.
I have troubles with a blueprintjs table with redux. Here I am trying to maintain the row selection thru redux. I am fairly new to react/redux so maybe I am missing something?
I tried the same component without redux (i.e. the selection modifies the state of the component) and it works fine, but I would prefer to use redux.
Here is a minimal example (jsx) with redux:
import React from "react"
import {Cell, Column, Table, SelectionModes, Regions, RegionLayer} from "#blueprintjs/table"
import ReactDOM from 'react-dom';
import {Provider, connect} from 'react-redux'
import {applyMiddleware, createStore} from 'redux';
import createLogger from 'redux-logger';
export class TableTestRedux extends React.Component {
constructor(props) {
super(props)
this.state={}
}
componentDidUpdate() {
console.log("TableTestRedux:componentDidUpdate props:", this.props)
}
onSelection(e) {
console.log("TableTestRedux:onSelection e:", e)
if (e.length > 0) {
var selectedRow = e[0].rows[0]
this.props.onSelectedEdgeOverrideIdx(selectedRow)
}
}
getSelectedRegion() {
var region = this.props.selectedEdgeOverrideIdx != null ? [Regions.row(this.props.selectedEdgeOverrideIdx)] : []
console.log('TableTestRedux:getSelectedRegion returns ', region);
return region
}
render() {
console.log('TableTestRedux:render props:', this.props);
const renderCell = (rowIndex) => <Cell>{this.props.edgeOverrides[rowIndex]}</Cell>;
return (
<div>
<Table ref="table" numRows={this.props.edgeOverrides.length} onSelection={(e) => this.onSelection(e)}
allowMultipleSelection={false}
selectionModes={SelectionModes.ROWS_ONLY}
selectedRegions={this.getSelectedRegion()}>
<Column name="Description" renderCell={renderCell}/>
</Table>
</div>
)
}
}
// --- redux container/smart component
const mapStateToProps = (state) => {
console.log("TableTestRedux:mapStateToProps ", state);
return {
edgeOverrides: state.edgeOverrides ? state.edgeOverrides : [],
selectedEdgeOverrideIdx: state.selectedEdgeOverrideIdx
}
}
const mapDispatchToProps = (dispatch) => {
console.log("TableTestRedux:mapDispatchToProps ");
return {
onSelectedEdgeOverrideIdx: (selectedEdgeOverrideIdx) => {
dispatch(OverrideEntryActions.selectEdgeOverrideIdx(selectedEdgeOverrideIdx))
}
}
}
export const TableTestReduxCC = connect(mapStateToProps, mapDispatchToProps)(TableTestRedux)
// --- redux action and reducer
export class OverrideEntryActions {
static selectEdgeOverrideIdx(selectedEdgeOverrideIdx) {
return {
type: 'SELECT_EDGE_OVERRIDE_IDX',
selectedEdgeOverrideIdx: selectedEdgeOverrideIdx
}
}
}
const initialOverrideEntryState = {
selectedEdgeOverrideIdx: null,
edgeOverrides: ["bla", "blabla", "more blabla"]
}
export const overrideEntryReducer = (state = initialOverrideEntryState, action) => {
console.log("overrideEntryReducer: action:", action, " state:", state)
switch (action.type) {
case 'SELECT_EDGE_OVERRIDE_IDX':
return {selectedEdgeOverrideIdx: action.selectedEdgeOverrideIdx}
default:
return state
}
}
// --- launch
var store = createStore(overrideEntryReducer, applyMiddleware(createLogger()))
ReactDOM.render((
<Provider store={store}>
<TableTestReduxCC/>
</Provider>
), document.getElementById('app'))
When I click on a row header, TableTestRedux.render() is called and this causes all the table cells to be blank. The log shows:
Warning: NaN is an invalid value for the height css style property. Check the render method of RegionLayer.
The problem was on my side in overrideEntryReducer, I was erasing the edgeOverrides property of the state. Here is the fix:
case 'SELECT_EDGE_OVERRIDE_IDX':
return {...state,selectedEdgeOverrideIdx: action.selectedEdgeOverrideIdx}
If have a list of users and each Entry has a button »EDIT«. If the user clicks on it the following happens:
request the server for the form
Add the component <UserEditForm /> to the entry, what expands the entry
This works fine except one thing: If one clicks further buttons each Instance of the form receives the data of the last user form requested. That is because I have only only one userform property in the state.
So to solve this I want to exchange userform to userforms which should/could be an Object like that:
userforms: {
<id of first instance>: { … }, //formdata
<id of second instance>: { … },
…
}
But since I am new to React/Redux I do not really know how to do that, or what the »right« approach, or best practice is, to actually do it.
My Idea is to create a higher Order Component like so:
import React from 'react';
import {connect} from 'react-redux';
import {uuid} from '../../helpers/uuid';
export const formdatainstance = (FormInstance) => {
let id = null;
class FormDataMapper extends React.Component {
constructor (props) {
super(props);
id = uuid();
}
render () {
//extract the formdata from the state
//using the id
return <FormInstance { ...this.props } />
}
}
const mapStateToProps = (state) => {
console.log(id); //is null for one run
return {
userforms: state.userforms
};
};
return connect(mapStateToProps)(FormDataMapper);
}
So in the List component I can:
import UserEditForm from './UserEditForm';
import {formdatainstance} from './formdatainstance';
const MappedUserEditForm = formdatainstance(UserEditForm);
class List extends React.Component {
render(){
return (
{users.map(user => {
//more stuff
<MappedUserEditForm />
//more stuff
})}
);
}
}
So my Question: Is this a good Idea? If yes what would be the proper way to do the cleanup, so when in the life cycle of the component should I delete the data from the state? Is there another way to do that, which is easier?
Thanks for Help!
Here's what you can do...
import React from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { reduxForm } from 'redux-form';
class UserEditForm extends Component {
...
render() {
return <form onSubmit={this.props.handleSubmit(this.props.onSubmit)}>
...form fields
</form>
}
}
const mapStateToProps = (state, ownProps) => {
return {
form: ownProps.formId
}
}
export default compose(
connect(mapStateToProps),
reduxForm({
//...other redux-form options
})
)(UserEditForm);
Your ListComponent
render() {
return <ul>
{this.props.users.map(user =>
<li key={user.id}>
...
<UserEditForm formId={'form-' + user.id} onSubmit={...} />
</li>
)}
</ul>
}
This allows you to have a dynamic form name.
Even if the answer of #jpdelatorre seems to be the best hit for me, since it also includes the link to redux-forms, what will probably help me a lot, I would like to post my working solution here, just in case somebody might find it useful. It just hit me over night, so needed to test if my thought were right, what I could finally proof.
I was not able to do the whole Mapping with a sole HOC and I needed to add/modify reducers too. Basically it works that way:
Data Mapping is done by ID,
the original action creators are wrapped, such that the used id is attached to the Object
the reducers are wrapped two and called by the »datamapped« reducer
So the code of the original reducers and action creators does not need to be changed, what makes the wrapping kind of easy to use. I first wanted to use uuid's which are created on the fly, but I discarded that, to make possible to save and restore the whole application state.
so the HOC code is that:
import React from 'react';
import {connect} from 'react-redux';
// The Component to wrap,
// all of its actions
// its default state
export const formdatainstance = (FormInstance, Actions, defaultState = {}) => {
const mapStateToProps = (state) => {
return {
mappedData: state.mappedData
};
};
class FormDataMapper extends React.Component {
static propTypes = {
id: React.PropTypes.string.isRequired
};
static contextTypes = {
store: React.PropTypes.object
};
//most of mapping happens here
render () {
//wrap the action creators
const actions = Object.keys(Actions).reduce((list, key) =>{
list[key] = (...args) => {
const action = Actions[key](...args);
//handle asyn operations as well
if('then' in action && typeof action['then'] == 'function') {
action.then(data => {
//attaching the id
this.props.dispatch({...data, id: this.props.id});
});
} else {
//attach the id
this.context.store.dispatch({...action, id: this.props.id });
}
};
return list;
}, {}),
//there wont be any data at first, so the default state is handed
//over
mappedProps = this.props.mappedData.hasOwnProperty(this.props.id) ?
this.props.mappedData[this.props.id] : defaultState;
//merge the hotchpotch
let props = Object.assign({}, mappedProps, this.props, actions);
//clean up
delete props.id;
delete props.mappedData;
return <FormInstance { ...props } />
}
}
return connect(mapStateToProps)(FormDataMapper);
};
the reducer code:
//hlper method
export const createTypesToReducerMap = (types, reducer) => {
return Object.keys(types).reduce((map, key) => {
map[types[key]] = reducer;
return map;
}, {});
}
export const createMappedReducer = (reducerMap, defaultState = {}) => {
const HANDLERS = reducerMap.reduce((handlers, typeMap) => {
return { ...handlers, ...typeMap };
},{});
return (state, action) => {
if (!action.hasOwnProperty('id')) {
if (state === undefined) return defaultState;
return state;
}
const reducer = HANDLERS.hasOwnProperty(action.type) ?
HANDLERS[action.type] : null;
let a = {...action};
delete a.id;
return reducer !== null ?
Object.assign({}, state, { [action.id]: reducer(state[action.id], a)}) :
state;
}
}
and finally the store:
const userEditTypeReducerMap = createTypesToReducerMap(userEditTypes, userFormReducer);
const reducer = combineReducers({
…
mappedData: createMappedReducer(
[userEditTypeReducerMap], {})
…
});
export default compose(
applyMiddleware(
thunk
)
)(createStore)(reducer, {});
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.