I am getting mutation of state error between dispatches from redux - javascript

i am getting the following error:
A state mutation was detected between dispatches, in the path 'notifications.adhans.AsrAdhan'. This may cause incorrect behavior. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)]
Following is my notifications reducer in notifications.ts file.
import {
RESTORE_NOTIFICATIONS,
SET_ADHAN,
SET_ADHANTOGGLE,
SET_REMINDER,
SET_REMINDERTOGGLE,
} from "./../constants";
export const initialState = {
reminderToggle: true,
adhanToggle: true,
adhans: {
FajrAdhan: false,
DhuhrAdhan: true,
AsrAdhan: false, //--->getting mutation error for this
MaghribAdhan: true,
IshaAdhan: true,
},
reminders: {
FajrReminder: false,
DhuhrReminder: false,
AsrReminder: true,
MaghribReminder: false,
IshaReminder: true,
SunriseReminder: true,
MidnightReminder: false,
},
};
export default (state = initialState, action: AnyAction): notificationsT => {
switch (action.type) {
case SET_REMINDERTOGGLE:
return {
...state,
reminderToggle: action.payload,
};
case SET_ADHANTOGGLE:
return {
...state,
adhanToggle: action.payload,
};
case SET_ADHAN:
return {
...state,
adhans: {
...state.adhans,
[action.payload.key]: action.payload.value,
},
};
case SET_REMINDER:
return {
...state,
reminders: {
...state.reminders,
[action.payload.key]: action.payload.value,
},
};
case RESTORE_NOTIFICATIONS:
return { ...action.payload };
default:
return state;
}
};
error stack points at src\screens\Home.tsx:127:14 in RestoreState.
Following is the relevant code for Home.tsx
import AsyncStorage from "#react-native-async-storage/async-storage";
import * as actionTypes from "../store/constants";
import { initialState as notifInitState } from "../store/reducers/notifications";
...
const updatedNotificationsstate = { ...notifInitState };
const adhanKeys = Object.keys(notifInitState.adhans);
const keysArray = [
...adhanKeys,
...//other state keys
];
const Home = ({ navigation }) => {
const dispatch = useAppDispatch();
//When component mounts on start, Restore the redux state
React.useEffect(() => {
const RestoreState = async () => {
const savedState = await AsyncStorage.multiGet(keysArray);
savedState.forEach(([key, value]) => {
...
if(value){
//notifications
if (adhanKeys.includes(key))
return (updatedNotificationsstate.adhans[key] = JSON.parse(value));
...
}
});
dispatch({
type: actionTypes.RESTORE_NOTIFICATIONS,
payload: updatedNotificationsstate,
});
};
RestoreState();
}, []);
---
return (
<Layout>
..
</Layout>
);
};
export default Home;
what am i doing wrong here???
Please note I have removed typesrcipt types for javascript coders convenience.

updatedNotificationsstate.adhans[key] = JSON.parse(value) looks to be a mutation.
You've correctly created a shallow copy of updatedNotificationsstate but all the elements are still references to the originals in state, i.e. updatedNotificationsstate.adhans is a reference to the previous state. You must also shallow copy any nested state properties that are being updated.
if (adhanKeys.includes(key)) {
return (updatedNotificationsstate.adhans = { // <-- new object reference
...updatedNotificationsstate.adhans, // <-- shallow copy previous
[key]: JSON.parse(value) // <-- update property
});
}

Related

Redux: altering different parts of the initial state in Reducer according to Actions

I have the following Reducer:
const initialState = {}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return (action.dishes)
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return state
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return state
default:
return state;
}
};
export default dishReducer;
And the following action(s):
import {database} from '../../config/fbConfig'
export const startLoadingDishes = (dishes) => {
return (dispatch) =>{
return database.ref('products-dishes').once('value').then((snapshot) => {
let dishes = {}
snapshot.forEach((childSnapshot) => {
let parentkey = childSnapshot.key
let dishArray = [];
childSnapshot.forEach((dish) =>{
dishArray.push(dish.val())
});
dishes[childSnapshot.key] = dishArray;
})
dispatch(loadDishes(dishes))
}).then(() => {
dispatch({ type: 'LOAD_DISHES_SUCCESS' });
}).catch(err => {
dispatch({ type: 'LOAD_DISHES_ERROR' }, err);
});
}
}
export const loadDishes = (dishes) => {
return {
type: 'LOAD_DISHES',
dishes
}
}
The 'startLoadingDishes' action is called inside the componentDidLoad() of a certain Component. However, I want to alter the initial state of my dishReducer so that it includes additional information, as follows:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
So now 'action.dishes' returned by reducer [in 'LOAD_DISHES' case] should be put inside the 'value' part of the state, instead of it being the whole state. Also, the 'loaded' part of the state should be set to true if dishes have already been loaded earlier, and so on. I understand this is fairly simple but as I am new to React+Redux, I don't know how to alter the Action/Reducer codes properly (while keeping state immutability). Any help is appreciated.
I originally asked the question, here is how I solved it (not sure if this is the 'best' way though):
New reducer file:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return {
value: action.dishes,
loading: !state.loading,
loaded: false, //this might need to be set to true
error: false
}
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return {
...state, //or: state.value, as the other parts of state are being overwritten below
loaded: false,
loading: false,
error: true
}
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return {
...state, //better: state.value
loaded: true,
loading: false,
error: false
}
default:
return state;
}
};
export default dishReducer;
No change in actions file.
Now, inside the 'Main' component, I was originally accessing the state as such:
class Main extends Component {
componentDidMount() {
this.props.startLoadingDishes();
}
render() {
return (
//some code
)
}
}
const mapStateToProps = (state) => {
return {
dishes: state.dishes //to access dishes: dishes.value
}
}
export default connect(mapStateToProps, actions)(Main)
The Main component code also stayed the same, with the difference that now I use 'dishes.value' instead of just 'dishes' to access the value of dishes from the state (and dishes.loaded for loaded, and so on). And now the action caller inside componentDidMount is as follows:
componentDidMount() {
if(!this.props.dishes.loaded){
this.props.startLoadingDishes();
console.log("loading dishes from database")
}
}

React-Redux: Assigning a modified array to a property in the state

I am working on a React application and I am using Redux to store the state. I have the following the code:
menu.actions.js:
import { apiUrl, apiConfig } from '../../util/api';
import { ADD_CATEGORY, GET_MENU } from './menu.types';
export const getMenu = () => async dispatch => {
const response = await fetch(`${apiUrl}/menu`);
if (response.ok) {
const menuData = await response.json();
dispatch({ type: GET_MENU, payload: menuData })
}
}
export const addCategory = category => async dispatch => {
const options = {
...apiConfig(),
method: 'POST',
body: JSON.stringify(category)
};
const response = await fetch(apiUrl + '/category/', options)
let data = await response.json()
if (response.ok) {
dispatch({ type: ADD_CATEGORY, payload: { ...data } })
} else {
alert(data.error)
}
}
menu.reducer.js:
import { ADD_CATEGORY, GET_MENU } from './menu.types';
const INITIAL_STATE = []
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case GET_MENU:
return [...action.payload];
case ADD_CATEGORY:
return [ ...state, { ...action.payload, menuItem: [] } ]
default:
return state;
}
}
In the above Reducer, the initial state is an empty array. Dispatching the GET_MENU action, changes the initial state so that it contains an array of menu items instead.
The array that is fetched in the GET_MENU action is of the following:
I have modified the code in the Reducer function so that the initial state is now the following:
menu.reducernew.js:
import { ADD_CATEGORY, GET_MENU } from './menu.types';
const INITIAL_STATE = {
menuArray: [],
isSending: false
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case GET_MENU:
return {
...state,
menuArray: action.payload
};
case ADD_CATEGORY:
return {
...state,
menuArray: [ ...menuArray, { ...action.payload, menuItem: [] } ]
};
default:
return state;
}
}
For the case ADD_CATEGORY in the Reducer, I am not sure what the correct syntax is for reassigning the menuArray property in the state to the modified array. I want the array to have the new object that is fetched within the addCategory action creator added to it.
When I run my application, I am getting the following error:
I am not sure why I am getting this error or how to resolve it. Any insights are appreciated.
Here menuArray: [ ...menuArray, { ...action.payload, menuItem: [] } menuArray variable is not defined which you are trying to spread. Use state.menuArray to access current menuArray.
Change:
menuArray: [ ...menuArray, { ...action.payload, menuItem: [] }
To:
menuArray: [ ...state.menuArray, { ...action.payload, menuItem: [] }
case ADD_CATEGORY:
return {
...state,
menuArray: [ ...state.menuArray, { ...action.payload, menuItem: [] } ]
};

Cannot change redux boolean state

I feel little confused, the problem is defineAvailableTouch action and state update connected to it.
Here is my code:
Actions/index.js
import {
ANIMATE_HELLO,
HANDLE_SCROLL,
IS_TOUCH_DEVICE,
SET_ABOUT_TOP,
SET_CONTACT_TOP,
SET_PORTFOLIO_TOP
} from "../Constants/ActionTypes";
export const animateHello = hello => ({
type: ANIMATE_HELLO,
payload: hello
});
export const handleScroll = scrollDelta => ({
type: HANDLE_SCROLL,
payload: scrollDelta
});
export const defineTouchAvailable = isTouchDevice => ({
type: IS_TOUCH_DEVICE,
payload: isTouchDevice
});
export const setAboutTop = aboutTop => ({
type: SET_ABOUT_TOP,
payload: aboutTop
});
export const setContactTop = contactTop => ({
type: SET_CONTACT_TOP,
payload: contactTop
});
export const setPortfolioTop = portfolioTop => ({
type: SET_PORTFOLIO_TOP,
payload: portfolioTop
});
Reducers/index.js
import {
IS_TOUCH_DEVICE,
} from "../Constants/ActionTypes";
import { initialState } from "../Constants/InitialState/InitialState";
export const rootReducer = (state = initialState, action) => {
switch(action.type) {
case ANIMATE_HELLO:
return {
...state,
hello: action.payload
};
case HANDLE_SCROLL:
return {
...state,
scrollState: action.payload
};
case IS_TOUCH_DEVICE:
console.log(action.payload); //!!!!!! THIS PRINTS EXPECTED VALUE !!!!!!!!!!
return {
...state,
isTouchDevice: action.payload
};
case SET_ABOUT_TOP:
return {
...state,
aboutTop: action.payload
};
case SET_CONTACT_TOP:
return {
...state,
contactTop: action.payload
};
case SET_PORTFOLIO_TOP:
return {
...state,
portfolioTop: action.payload
};
default:
return state
}
};
InitialState.js
export const initialState = {
scrollState: 0,
hello: 'H',
aboutTop: 0,
portfolioTop: 0,
contactTop: 0,
isTouchDevice: true
};
App.js
import React, { Component } from 'react';
import { connect } from "react-redux";
import About from "./Containers/About";
import Contact from "./Containers/Contact";
import Page from "./Containers/Page";
import Projects from "./Containers/Projects";
import {
defineTouchAvailable,
handleScroll
} from "./Actions";
window.onbeforeunload = () => {
handleScroll(0);
document.documentElement.scrollTop = 0;
};
const mapStateToProps = state => {
return {
isTouchDevice: state.isTouchDevice
}
};
const dispatchStateToProps = dispatch => {
return {
defineTouchAvailable: isTouchDevice =>
dispatch(defineTouchAvailable(isTouchDevice)),
handleScroll: scrollState => dispatch(handleScroll(scrollState))
}
};
class App extends Component {
componentDidMount() {
try {
document.createEvent('touchevent');
this.props.defineTouchAvailable(true);
} catch(e) {
this.props.defineTouchAvailable(false);
}
console.log(this.props.isTouchDevice); //!!!!!!!!!!!!!!! THIS ALWAYS PRINTS VALUE FROM initialState !!!!!!!!!!!!!!
if(this.props.isTouchDevice) {
document.documentElement.scroll(0, 1);
}
document.addEventListener('scroll', () => {
if (document.documentElement.scrollTop === 0) {
this.props.handleScroll(0);
}
});
}
render() {
return (
<div>
<Page/>
<Projects/>
<About/>
<Contact/>
</div>
);
}
}
export default connect(mapStateToProps, dispatchStateToProps)(App);
I really can't figure out whats wrong here.
As I commented
reducer console.log prints correct value that is expected to be assigned to my state (isTouchDevice field), but
after assigning it in dispatch action nothing changes - it is always value from initialState.
Can someone please explain it to me? Do I change my redux state uncorrectly? Then why other actions work as they're expected to?
The updated value of isTouchDevice will be available in componentDidUpdate, render or componentWillReceiveProps, not in componentDidMount.
componentDidMount will only be called one time when your component is mounted.
Note: componentWillReceiveProps is deprecated, better to not use it.

Redux data undefined

In logger seen anything is normal data loaded successfully.
You can see it by follow this link https://i.stack.imgur.com/uTLpr.jpg
But why this crypto props is undefined and how to fix it.
Does anyone have any idea?
Component
class CryptoContainer extends Component {
componentWillMount(){
this.props.Fetchcoin()
}
renderCoinCards() {
const { crypto } = this.props;
console.log('Crypto : '+crypto)
return crypto.data.map((coin,index) => //Add this
<CoinCard
key={coin.name}
coin_name={coin.name}
symbol={coin.symbol}
price_usd={coin.price_usd}
percent_change_24h={coin.percent_change_24h}
percent_change_7d={coin.percent_change_7d}
/>
)
}
}
function mapStatetoProps(state){
return{
crypto:state.crypto
}
}
export default connect(mapStatetoProps,{Fetchcoin})(CryptoContainer)
>
Action
export default function FetchCoin(){
return dispatch => {
dispatch({type:FETCHING_COIN_DATA})
return axios.get(`https://api.coinmarketcap.com//v1/ticker/?limit=10`)
.then(res => {
dispatch({type:FETCHING_COIN_DATA_SUCCESS,payload:res.data})
})
.catch(err => {
dispatch({type:FETCHING_COIN_DATA_FAIL,payload:err.data})
})
}
}
Reducer
const initialState = {
isFetching: null,
data: [],
hasError: false,
errorMessage: null
}
export default function (state = initialState, action) {
switch (action.type) {
case FETCHING_COIN_DATA:
return Object.assign({}, state, {
isFetching: true,
data: null,
hasError: false,
errorMessage: null
})
case FETCHING_COIN_DATA_SUCCESS:
return Object.assign({}, state, {
isFetching: false,
data: action.payload,
hasError: false,
errorMessage: null
})
case FETCHING_COIN_DATA_FAIL:
return Object.assign({}, state, {
isFetching: false,
data: action.payload,
hasError: true,
errorMessage: action.err
})
default:
return state
}
}
And store
const middleware = applyMiddleware(thunk,promise,logger)
const rootReducer = combineReducers({
crypto:CyptoReducer
})
const Store = createStore(
rootReducer,middleware
)
export default Store
I fixed misspelling in store but when I add crypto.data.map() in renderCoinCard
data seem still not working debugger shown
TypeError: Cannot read property 'map' of null
I think there is a problem with your declaration of rootReducer.Here you have mentioned "cypto" instead of "crypto". Fix this and it will work as here only redux declares a reducer and as you have declared here as "cypto" it expects "cypto" and fails.

returning nested state with redux and thunk

I'm fairly new to redux & thunk, and have been following tutorials to try and understand, and am managing to work it into my app ok. One thing i'm not understanding, is how i can get several state objects on the root level into one nested object. For example, right now my state looks like:
{
timeline: [Array] // My timeline data in an array of objects
timelineHasErrored: false,
timelineIsLoading: false
}
But what I really want is:
{
timeline : {
data: [Array] // My timeline data in an array of objects
hasErrored: false,
isLoading: false
}
}
and i'm really not quite sure how to nest these, or what the proper way to do that is. Below is my redux code, it's pretty simple so i'll post it all.
Reducers index
import { combineReducers } from 'redux'
import { timeline, timelineHasErrored, timelineIsLoading } from './timeline'
export default combineReducers({
timeline, timelineHasErrored, timelineIsLoading
});
Timeline Reducers
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
export function timelineHasErrored(state = false, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return action.hasErrored;
default:
return state;
}
}
export function timelineIsLoading(state = false, action) {
switch (action.type) {
case TIMELINE_IS_LOADING:
return action.isLoading;
default:
return state;
}
}
export function timeline(state = [], action) {
switch (action.type) {
case TIMELINE_FETCH_DATA_SUCCESS:
return action.timeline;
default:
return state;
}
}
Actions
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
import api from '../services/api'
export function timelineHasErrored(bool) {
return {
type : TIMELINE_HAS_ERRORED,
hasErrored : bool
}
}
export function timelineIsLoading(bool) {
return {
type : TIMELINE_IS_LOADING,
isLoading : bool
}
}
export function timelineFetchDataSuccess(timeline) {
return {
type : TIMELINE_FETCH_DATA_SUCCESS,
timeline
}
}
export function timelineFetchData() {
return dispatch => {
dispatch( timelineIsLoading(true) )
api.getTracks().then(
res => {
dispatch( timelineIsLoading(false) )
dispatch( timelineFetchDataSuccess(res.body) )
},
err => {
dispatch( timelineIsLoading(false) )
dispatch( timelineHasErrored(true) )
}
)
}
}
And then in my react component I format the object like how i want it... but i think it would be better to have it nested in the actual state so i'm not creating extra work for myself if things change
// Redux State
const mapStateToProps = (state) => {
const obj = {
timeline : {
data : state.timeline,
hasErrored: state.tracksHasErrored,
isLoading: state.tracksIsLoading
}
}
return obj
}
// Redux Dispatch
const mapDispatchToProps = (dispatch) => {
return {
fetchData: () => dispatch( timelineFetchData() )
}
}
If anybody has any tips or corrections for me bring em on, i'm trying to get a solid grasp on redux, thanks!
Your timeline reducer is pretty small, so you could have it as a single reducer as follows:
const initialState = {
data: [],
hasErrored: false,
isLoading: false
};
export function timeline(state = initialState, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return {
...state,
hasErrored: action.hasErrored
};
case TIMELINE_IS_LOADING:
return {
...state,
isLoading: action.isLoading
};
case TIMELINE_FETCH_DATA_SUCCESS:
return {
...state,
data: action.timeline
};
default:
return state;
}
}
Then you wouldn't need to call combineReducers(), unless you had other reducers.

Categories

Resources