Redux mutation detected between dispatches - reactjs

I'm having some trouble with a react redux I'm currently working on. I'm relatively new to Redux so maybe I'm missing a simple concept here but what I'm trying to do is build a deck building app for a card game and I want to be able to save the deck anytime a user adds or removes a card from their deck.
However, anytime I click add or remove I'm receiving the following error message while trying to dispatch an update action.
The error message reads as follows:
Uncaught Error: A state mutation was detected between dispatches, in the path `decks.0.cards.mainboard.0.quantity`. This may cause incorrect behavior.
My container component
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import DeckMobileDisplay from './DeckMobileDisplay';
import * as deckActions from '../../actions/deckActions';
export class DeckEditorContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
deck: Object.assign({}, this.props.deck)
}
this.addCard = this.addCard.bind(this);
this.removeCard = this.removeCard.bind(this);
}
addCard(board, cardName) {
let deck = this.state.deck;
let cards = this.state.deck.cards;
cards[board].forEach(i => {
if(i.name === cardName)
i.quantity += 1;
});
const update = Object.assign(deck, cards);
this.props.deckActions.updateDeck(update).then(deck => {
console.log(deck);
})
.catch(err => {
console.log(err);
});
}
removeCard(board, cardName) {
let deck = this.state.deck;
let cards = this.state.deck.cards;
cards[board].forEach(i => {
if(i.name === cardName) {
if (i.quantity === 1) {
cards[board].splice(cards[board].indexOf(i), 1);
}
else {
i.quantity -= 1;
}
}
});
const update = Object.assign(deck, cards);
this.props.deckActions.updateDeck(update).then(deck => {
console.log(deck);
})
.catch(err => {
console.log(err);
});
}
render() {
const deck = Object.assign({}, this.props.deck);
return (
<div className="editor-container">
<DeckMobileDisplay
deck={deck}
addCard={this.addCard}
removeCard={this.removeCard}
/>
</div>
);
}
}
DeckEditorContainer.PropTypes = {
deck: PropTypes.object
};
function getDeckById(decks, id) {
const deck = decks.filter(deck => deck.id == id);
if (deck.length) return deck[0];
return null;
}
function mapStateToProps(state, ownProps) {
const deckId = ownProps.params.id;
let deck = {
id: '',
userId: '',
cards: []
}
if (state.decks.length > 0) {
deck = getDeckById(state.decks, deckId);
}
return {
deck: deck
};
}
function mapDispatchToProps(dispatch) {
return {
deckActions: bindActionCreators(deckActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(DeckEditorContainer);
Component for DeckMobileDisplay
import React, {PropTypes} from 'react';
import TabContainer from '../common/Tabs/TabContainer';
import Tab from '../common/Tabs/Tab';
import CardSearchContainer from '../CardSearch/CardSearchContainer';
import DeckList from './DeckList.js';
class DeckMobileDisplay extends React.Component {
render() {
return (
<TabContainer>
<Tab title="DeckList">
<DeckList
deck={this.props.deck}
addCard={this.props.addCard}
removeCard={this.props.removeCard}
/>
</Tab>
<Tab title="Search">
<CardSearchContainer
addCard={this.props.addCard}
removeCard={this.props.removeCard}
/>
</Tab>
<Tab title="Stats">
<p>stats coming soon...</p>
</Tab>
</TabContainer>
);
}
}
DeckMobileDisplay.propTypes = {
deck: PropTypes.object.isRequired,
addCard: PropTypes.func.isRequired,
removeCard: PropTypes.func.isRequired
}
export default DeckMobileDisplay;
Related Actions
export function createDeck(deck) {
return dispatch => {
dispatch(beginAjaxCall());
const config = {
method: 'POST',
headers: { 'Content-Type' : 'application/json' },
body : JSON.stringify({deck: deck})
};
return fetch(`http://localhost:3000/users/${deck.userId}/decks`, config)
.then(res => res.json().then(deck => ({deck, res})))
.then(({res, deck}) => {
if (res.status >= 200 && res.status < 300) {
dispatch(createDeckSuccess(deck.deck));
}
else
dispatch(createDeckFailure(deck));
})
.catch(err => {
console.log(err);
dispatch(ajaxCallError(err));
});
};
}
export function updateDeck(deck) {
return dispatch => {
dispatch(beginAjaxCall());
const body = JSON.stringify({deck: deck});
const config = {
method: 'PUT',
headers : { 'Content-Type' : 'application/json' },
body: body
};
return fetch(`http://localhost:3000/decks/${deck.id}`, config)
.then(res => res.json().then(deck => ({deck, res})))
.then(({res, deck}) => {
if (res.status >= 200 && res.status < 300) {
dispatch(updateDeckSuccess(deck.deck));
}
dispatch(ajaxCallError(err));
})
.catch(err => {
console.log(err);
dispatch(ajaxCallError(err));
});
};
}
export function updateDeckSuccess(deck) {
return {
type: types.UPDATE_DECK_SUCCESS,
deck
};
}
And my deck Reducer
import * as types from '../actions/actionTypes';
import initialState from './initialState';
export default function deckReducer(state = initialState.decks, action) {
switch (action.type) {
case types.LOAD_USERS_DECKS_SUCCESS:
return action.decks;
case types.CREATE_DECK_SUCCESS:
return [
...state,
Object.assign({}, action.deck)
]
case types.UPDATE_DECK_SUCCESS:
return [
...state.filter(deck => deck.id !== action.deck.id),
Object.assign({}, action.deck)
]
default:
return state;
}
}
If you need to see more of the app the repo is here:
https://github.com/dgravelle/magic-redux
Any kind of help would be appreciated, thanks!

Your problem is caused because you are modifying component's state manually.
One Redux's principle is:
State is read-only
The only way to change the state is to emit an action, an object
describing what happened.
This ensures that neither the views nor the network callbacks will
ever write directly to the state. Instead, they express an intent to
transform the state. Because all changes are centralized and happen
one by one in a strict order, there are no subtle race conditions to
watch out for. As actions are just plain objects, they can be logged,
serialized, stored, and later replayed for debugging or testing
purposes.
In the method removeCard you are modifying the state:
removeCard(board, cardName) {
let deck = this.state.deck;
//This is just a reference, not a clone
let cards = this.state.deck.cards;
cards[board].forEach(i => {
if(i.name === cardName) {
if (i.quantity === 1) {
//Here you are modifying cards, which is a pointer to this.state.deck.cards
cards[board].splice(cards[board].indexOf(i), 1);
}
else {
//Here you are modifying cards, which is a pointer to this.state.deck.cards
i.quantity -= 1;
}
}
});
//... more stuff
}
One concept you might be missing is that this.state.deck.cards is a reference/pointer to the Array's memory position. You need to clone it if you want to mutate it.
One solution could be to clone the original array instead:
removeCard(board, cardName) {
let deck = this.state.deck;
//Here you are cloning the original array, so cards references to a totally different memory position
let cards = Object.assign({}, this.state.deck.cards);
cards[board].forEach(i => {
if(i.name === cardName) {
if (i.quantity === 1) {
cards[board].splice(cards[board].indexOf(i), 1);
}
else {
i.quantity -= 1;
}
}
});
//... more stuff
}
Hope it helps you.

Related

react redux thunk not populating state object

im having an issue with my code, its not populating the state object when state action is being performed. im new with redux
i have this code. so far that having an issue
this is the statement that will called the props.action fetchProjectFamilyList
case 'SubBusinessUnit':
setProductFamilyDetailsObj([])
if (selectedOption.id != 0) {
props.actions.fetchDepartment(selectedOption.id)
props.actions.fetchProjectFamilyList(selectedOption.id)
console.log(props)
}
setDropdownDataInState(resetData, 'Department')
setFormFields({
...formFields,
'OtherNamedInsuredIndustry': {
...formFields.OtherNamedInsuredIndustry,
value: ''
},
'NamedInsuredIndustry': {
...formFields.NamedInsuredIndustry,
value: "",
selectedId: 0
},
[fieldName]: {
...formFields[fieldName],
value: selectedOption.description, selectedId: selectedOption.id
}
});
break;
and this is the code for the commonreducer
export const fetchProjectFamilyList = createAsyncThunk(types.FETCH_PROJECT_FAMILY_LIST,
async (option, {getState, rejectWithValue}) => {
const reduxThunkConfig = {
checkStateData:getState().commonReducer.projectFamilyList && getState().commonReducer.projectFamilyList[option],
rejectWithValue
}
const APIConfig = {
URL: "eapi-referencedata/v1/lists/12?filterBySourceList=" + option + "&filterBySourceListValue=15",
method:"getData",
}
console.log('fetchProjectFamilyList')
return fetchCachedData(reduxThunkConfig, APIConfig);
}
)
im using the builder in my case of course inistailstate is set
const initialState = {
projectFamilyList:{},
}
builder.addCase(fetchProjectFamilyList.fulfilled, (state, action) => {
const subDivision = action.meta.arg;
return {
...state,
projectFamilyList:{
...state.projectFamilyList,
[subDivision]: action.payload},
}})
const commonActions = { ...actions, fetchProjectFamilyList }
export { commonActions, commonReducer}
this is the comment that accept the state as props. but the props productFamilyDetailsObj is empty object
<ProductFamilyComponent
productFamilyDetailsObj={productFamilyDetailsObj}
/>
function ProductFamilyComponent({ productFamilyDetailsObj }) {
return <div className="boxLayout">
<p className="smallHeading">Product Families</p>
{productFamilyDetailsObj.map((text, textIndex) => {
let index = textIndex;
return ( .... and so on
I hope theres someone who could help me resolving this. thank in advance.

array.map from redux won't return data on the first load of react component

Hi I've been trying out a bit of react and electron and I'm just trying to make a media playlist type of application but I am having a bit of trouble with Redux
So I have setup the actions and they do kind of work but for some reason on the first initial load of a component the array.map that I am using to display all the results in a list won't actually render.
I have got the console out putting the results and when the component renders the first pass of the render fucntion the initial state is null then on the second pass the console logs the correct output but the array.map is still not outputting anything.
Then when I save a file in my editor (hot reload is on) I will then get an item to render on from the array.map
I can't work out if I have made astupid mistake somewhere or if I am just completly doing the wrong thing. So I'm hoping maybe someone might be able to shed some light on the situation.
Here is my component file with this array.map function that isn't working
interface Props {
songs?: any;
receiveMedia?: Function;
}
interface SongState {}
export default class Songs extends React.Component<Props, SongState> {
private initLoad = 0;
constructor(props: Props) {
super(props);
this.state = {};
this.getThumbnailRender = this.getThumbnailRender.bind(this);
}
componentDidMount() {
this.props.receiveMedia && this.props.receiveMedia();
}
getThumbnailRender() {
console.log(this.props.songs);
if (this.props.songs.Media.songs !== null) {
return this.props.songs.Media.songs.map((media: any) => {
if (media.extension !== "mp4") {
return (
<li
className={css.thumbNail}
id={media.id}
key={`${"media_thumb_"}${media.id}`}
>
<img src={mp3} />
<span className={css.floatingText}>{media.fileName}</span>
</li>
);
} else {
return (
<li
className={css.thumbNail}
id={media.id}
key={`${"media_thumb_"}${media.id}`}
>
<img src={media.filePath} />
<span className={css.floatingText}>{media.fileName}</span>
</li>
);
}
});
}
return <div>You haven't added any songs</div>;
}
render() {
return (
<div className={css.container}>
<h1>Songs</h1>
<div className={css.songHolder}>
<ul>{this.getThumbnailRender()}</ul>
</div>
</div>
);
}
}
I'm pretty sure that the Action and the reducer files are fine as they work later on but I will include them just incase I have made a stupid mistake
ACTIONS.ts
import { Songs } from "../../Models";
var fs = require("fs");
export enum ActionTypes {
MEDIA_RECEIVED = "[Media] MEDIA_RECEIVED"
}
export interface MediaReceived {
type: ActionTypes.MEDIA_RECEIVED;
payload: {
globals: Songs;
};
}
export function mediaReceived(json: any): MediaReceived {
return {
type: ActionTypes.MEDIA_RECEIVED,
payload: {
globals: json
}
};
}
function loadInCurrentSongList() {
var obj: Songs = {
//#ts-ignore
songs: []
};
fs.readFile("saveFile.json", "utf-8", (err: any, data: any) => {
if (err) {
alert("An error ocurred reading the file :" + err.message);
return;
}
const newData = JSON.parse(data);
if (newData.songs.length > 0) {
newData.songs.map((song: any) => {
obj.songs.push(song);
});
}
});
return obj;
}
export function receiveMedia() {
return (dispatch: Function) => {
dispatch(mediaReceived(loadInCurrentSongList()));
};
}
export type Action = MediaReceived;
REDUCER.ts
import { Songs } from "../../Models";
import { Action, ActionTypes } from "./Actions";
export interface State {
songs: Songs | null;
}
export const initialState: State = {
songs: null
};
export function reducer(state: State = initialState, action: Action) {
if (action.type === ActionTypes.MEDIA_RECEIVED) {
return Object.assign({}, state, action.payload.globals);
} else {
return state;
}
}
Thank you very much :)
Because fs.readFile is async and the callback passed to it is executed at a later time when the fs operation completes, obj is being returned from loadInCurrentSongList before it is populated with the songs, and therefore when mediaReceived is dispatched songs is still empty. Your debugger is fooling you a bit because it displays the updated value of obj after it gets populated in the fs.readFile callback.
The hot reload works because it forces a re-render without destroying state, at which point obj has been mutated inside of the fs.readFile callback.
Here's one option to manage the async nature of fs.readFile with a Promise such that you wait for it complete instead of mutating the obj returned from loanInCurrentSongList. Not super familiar with typescript so you'll have to update the types probably:
function loadInCurrentSongList() {
return new Promise(resolve => {
fs.readFile("saveFile.json", "utf-8", (err: any, data: any) => {
var obj: Songs = {
//#ts-ignore
songs: []
};
if (err) {
alert("An error ocurred reading the file :" + err.message);
resolve(obj);
return;
}
const newData = JSON.parse(data);
if (newData.songs.length > 0) {
newData.songs.map((song: any) => {
obj.songs.push(song);
});
}
resolve(obj);
});
}
export function receiveMedia() {
return (dispatch: Function) => {
loadInCurrentSongList().then(songList => {
dispatch(mediaReceived(songList));
}
};
}

ReactJS, componentWillReceiveProps can get data, but render cannot

I am working on a ReactJS 15 project using sagas and reselect as middle ware to fetch data. I can successfully get data in componentWillReceiveProps and set the data in the state, however there still not data in the render function first run when I take the data from the state. Anyone knows what's going on here? BTW, I used json-server as mock data server.Below is part of my code:
Component:
constructor(props) {
super(props);
this.state = {
timelineData: [],
};
}
componentDidMount() {
// use react redux to make the api call here
this.props.fetchTimelineData({
id: parse(this.props.location.search.substr(1)).id,
});
}
componentWillReceiveProps(nextProps) {
console.log('nextProps', nextProps);
// Successfully take the data from the nextProps (timelineData is not [])
const { timelineData } = nextProps;
this.setState({
timelineData,
});
}
render() {
// the first render called timelineData is an empty array, which will not able to populate the UI
// which RaiseTimeline is empty
const { timelineData } = this.state;
return (
<RaiseTimelineStyled>
<RaiseDetailsGrid>
<Accordion
title={
<RaiseAccordionHeader image={image} title={'Timeline'} />
}>
<Timeline.wrapper>
<RaiseTimelineStyled.divider>
<RaiseTimelineStyled.container>
<RaiseTimeline timelineEvents={timelineData} />
action.js (works fine):
export const setTimelineData = timelineData => console.log('actions.js', timelineData) || ({
type: ACTIONS.SET_TIMELINE_DATA,
timelineData,
});
Api.js (works fine):
class TimelineAPI {
// payload will be used after backend done
static fetchTimelineData(payload) {
return http.get(`${baseURI}/timeline`).then(result => console.log('api', result.data) || result.data);
}
}
Reducers: (works fine)
function TimelineDataReducer(state = initialState, action) {
switch (action.type) {
case ACTIONS.SET_TIMELINE_DATA:
console.log('reducer', action.timelineData);
return state.set('numbers', action.timelineData);
default:
return state;
}
}
Sagas: (works fine)
export function* fetchTimelineData(action) {
yield put(togglePendingScreen(true));
const { result, error } = yield call(TimelineAPI.fetchTimelineData, action.payload);
if (error) {
yield put(
toggleErrorModal({
isOpen: true,
text: error.code,
source: 'Fetch Timeline Data',
}),
);
} else {
console.log('Sagas', result.timeline);
yield put(ACTIONS.setTimelineData(result.timeline));
}
yield put(togglePendingScreen(false));
}
Selectors(works fine):
import { createSelector } from 'reselect';
const selectTimelineData = state => state.get('TimelinePageData').toJS();
const selectTimeline = () =>
createSelector(selectTimelineData, TimelineDataState => TimelineDataState.numbers);
export { selectTimeline };
To me it seems logical that you have no data on the first run.
The reason is there render() function is called once before the componentDidMount() in the react life cycle. (V15)
Look here for the react 15 life cycle : https://gist.github.com/bvaughn/923dffb2cd9504ee440791fade8db5f9
I got the answer, everything is correct, but another person name but another person name same component and set its state in side the constructor, so it's not able to render in the first time

React Redux: Update and replace records with another records returns value of 1

React Redux: Update and replace records with another records returns value of 1.
On the server side, I have a json response [{"id":"10", "food_name":"Rice"}]
The code below works fine by displaying a food item called Rice from the database via API Call as showed in the json array above.
Now I have a requirements to replace the displayed food item Rice with Beans.
To this effect, I have a json files which is to be returned via API Call after posting
[{"id":"10", "food_name":"Beans"}]
I have also created a Post button which should send data to the server side and return the response Beans.
Here is my effort as well as my Issue which is caused by reducer.
If Implement the code below in the reducer
case foodConstants.FOOD_SUCCESS_POST:
return {
items: state.items.map(food1 => {
if (food1.id === action.id) {
//return { ...food1, food_name: state.items[0].food_name};
return { ...food1, food_name: 'Beans' };
}
The Code works fine and Rice is replaced with Beans since I set value beans in the reducer.
but since I need to get the records via API Call so if implement
case foodConstants.FOOD_SUCCESS_POST:
return {
items: state.items.map(food1 => {
if (food1.id === action.id) {
return { ...food1, food_name: state.items[0].food_name};
}
Am getting value of 1 replacing Rice instead of Beans. Please where is this value of 1 coming from.
I need to have beans replace record Rice as a value returned from API Call.
My action and service code are okay as I can see the json returned records in the array as per
[{"id":"10", "food_name":"Beans"}]
I think my problem lies in this line of code below which returns value of 1 instaed of Beans.
return { ...food1, food_name: state.items[0].food_name};
Here is the full code
import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { foodActions } from 'actions';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
this.props.dispatch(foodActions.getFood());
}
handleFood(id,food_type) {
return (e) => this.props.dispatch(foodActions.postfood(food_id));
}
render() {
const { food1, foods1 } = this.props;
return (
<div>
{foods1.items &&
<ul>
{foods1.items.map((food1, index1) =>
<li key={food1.id}>
{food1.food_name}
<input type="button" value="Post and Update Food Name" onClick={this.handleFood(food1.id)} />
</li>
)}
</ul>
}
</div>
);
}
}
function mapStateToProps(state) {
const { foods1} = state;
const { food1 } = state;
return {
food1, foods1
};
}
const connectedApp = connect(mapStateToProps)(App);
export { connectedApp as App };
Reducer Code
import { foodConstants } from '/constants';
export function foods1(state = {}, action) {
switch (action.type) {
case foodConstants.GETALL_REQUEST:
return {loading: true};
case foodConstants.GETALL_SUCCESS:
return {
loading: false,
error: null,
items: action.foods1,
};
case foodConstants.GETALL_FAILURE:
return {
error: action.error
};
// Post and Update Food Name
case foodConstants.FOOD_REQUEST_POST:
return {...state};
case foodConstants.FOOD_SUCCESS_POST:
return {
items: state.items.map(food1 => {
if (food1.id === action.id) {
return { ...food1, food_name: state.items[0].food_name};
}
return food1;
})
};
case foodConstants.FOOD_FAILURE_POST:
return {
error: action.error
};
default:
return state
}
}
You need to replace value that is coming in action, but you are picking from state
case foodConstants.FOOD_SUCCESS_POST: {
const updatedItems = state.items.map((food1) => {
if (food1.id === action.id) {
return { ...action };
}
return food1;
});
return { ...state, items: updatedItems };
}
Or you can do this as well
case foodConstants.FOOD_SUCCESS_POST: {
let updatedItems = { ...state.items };
const itemIndex = updatedItems.findIndex((food1) => food1.id === action.id);
if(itemIndex > -1){
updatedItems[itemIndex] = {
...updatedItems[itemIndex],
...action,
}
}
return { ...state, items: updatedItems };
}

How To Access Props In a functional components

I have some problem with component
When i use props i get same data in all of mates, but in GET_ODD action data is different
1)now how i can fix this porblem ?
2)how i can set interval for odd changes ? if this.props.odd.ad_time < nextProps.odd.ad_time change the odd in curenet props
json data for each odd :
{
"id": "24854545",
"home_od": "1.002",
"draw_od": "51.000",
"away_od": "51.000",
"ss": "3-1",
"time_str": "87",
"add_time": "1537706413"
}
reducer :
const initialState = {
odd: {}
};
function Markets(state = initialState, action) {
switch (action.type) {
case GET_ODD:
return {
...state,
...action.odd
};
default:
return state;
}
}
export default Markets
Markets action :
var marketsRESTRepository = new MarketsRESTRepository();
export const GET_ODD = "GET_ODD";
export function getOdd(odd) {
return {
type: GET_ODD,
odd
}
}
export function getAsyncOdd(eventId) {
return function (dispatch) {
marketsRESTRepository.getById(eventId).then(odd => {
dispatch(getOdd({odd}));
});
}
}
Action Odd data
same results for odds
in the market component try this :
import React, { Component } from "react";
import { connect } from 'react-redux';
import { getAsyncOdd } from "../../actions/markets.actions";
export class Markets extends Component {
constructor(props) {
super(props);
this.state = {
odds: {}
};
}
componentDidMount(){
this.props.getOdd(this.props.eventId);
this.setState({this.props.odd})
}
componentWillRecieveProps(nextProps) {
if(this.state.odds!==nextprops.odd){
this.setState({odds: nextprops.odd})
}
}
render() {
return (
<React.Fragment>
<span>{this.state.odds.home_od}</span>
<span>{this.state.odds.draw_od}</span>
<span>{this.state.odds.away_od}</span>
</React.Fragment>
);
}
}
function mapStateToProps({ markets }) {
return {
odd: markets.odd
}
}
function mapDispatchToProps(dispatch) {
return {
getOdd: (eventId) => dispatch(getAsyncOdd(eventId))
}
}
export default connect(mapStateToProps,
mapDispatchToProps
)(Markets)
Looks like the problem is with your reducer,
Why are you spreading the action? Since you are accessing it this way,
<span>{this.props.odd.home_od}</span>
Change it to this,
return {
...state,
add : action.odd
};
You have this:
dispatch(getOdd({odd}));
and the getOdd sync action will return this object:
{
type: "GET_ODD",
odd: {
odd: { /* data */}
}
}
which in turn will get dumped in the state by the reducer using the spread operator. I think you have to change the approach:
keep you getOdd action as is
when you call getOdd use it as getOdd(odd). No curly braces.
in you reducer, don't use the spread operator for the action. Use a specific key, for example:
return {
...state,
odd: action.odd
}
your async action looks good, just add a catch there and add a console.error so you know if something fails
For you 2 question. You can use one of the React life cycle methods to compare the current props and the next props coming and then perform that logic.
I hope it helps!

Resources