I've recently learned React and Redux, and right when I thought I was getting the hang of it I've been hung up on this.
In my reducer I'm under the impression that the spread operator should create a new instance of the object at a difference memory location. However, using === to compare the objects returns true which I believe means the objects are the same instances.
const initState = {
loadcases: {
1: {
isActive: false,
profile: 'UB'
},
2: {
isActive: true,
profile: 'PFC'
}
}
}
const designInput = (state=initState, action={}) => {
let newState = {...state}; //clone design input state
console.log(newState.loadcases === state.loadcases)
// Prints: TRUE ---- WHYYYYYYY????????????
// goes on to modify newState before returning it.
}
The redux logger is showing my previous state to already equal the new state which has me thinking that this here is interfering with the store directly. I've read through some redux docs and other posts which make me believe I have set everything else up correctly.
Any resources to educate me more on this issue is also much appreciated.
UPDATE:
Jayce's answer was exactly what was happening.
This SO post outlines which method for a deep clone will be suitable. For me, it's likely I'll want to store dates and other data so I've chosen to use lodash cloneDeep method.
So now: console.log(cloneDeep(state) === state.loadcases); // false
Your assumption that the spread operator creates a new object is true, however it doesn't do a deep clone of objects. It copies the references of nested values over to the new object, so that's why it's printing that they're equal.
If you want to deeply clone something with nested values you can't use spread, you have to use another method. You could use a library like lodash, or something such as JSON.parse(JSON.stringify(obj)), there's multiple options just google for something like "Javascript deep clone object"
Related
Recently I change to slice and I seeing some odd behavior can someone explain:
const initialState = {
[]
};
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {
loadUsers(state, action) {
state = state.concat(action.payload);
},
},
});
when i do something like this the state wont change and payload dont add to state array
i can go like
for(let i=0; i < action.payload.length; i++) {
state.push(action.payload[i]
}
and everything work fine, then I realized if name the state initial state like:
const initialState = {
users: [],
};
Then I can go like:
state.users = state.users.concat(action.payload);
on loadUsers reducer and this work fine too
can someone explain why concat wont work on first attempt when i have initial state of [] without naming it
The problem here is not push or concat, but the fact that
state = something
will never do anything.
Redux Toolkit watches the object in the variable state for modifications - but that assignment throws away that object and puts a new object into the variable instead of changing it.
That cannot be observed.
Instead, you can do
return something
For more information, see Writing Reducers with immer, especially resetting and replacing state, which says:
A common mistake is to try assigning state = someValue directly. This will not work! This only points the local state variable to a different reference. That is neither mutating the existing state object/array in memory, nor returning an entirely new value, so Immer does not make any actual changes.
Adding on to the previous answer, to make one distinction. You should not attempt to mutate the entire state as in the line
State = something
But you can and should mutate state properties in this way, in fact it is one of the major benefits of redux toolkit and immer, so
State.prop = something
Is ok.
This is a good time to reiterate this kind of state mutation is only ok in redux toolkit where immer is doing it’s work.
Again here is the link to the relevant docs
https://redux-toolkit.js.org/usage/immer-reducers#resetting-and-replacing-state
I have nested arrays in my props, and the user can re-order the second-level array by dragging and dropping column. I (using redux) make a shallow copy of getState's outer array, do the re-ordering (through re-assignment of array objects based on index), and return the new array to the state (using spread operator in my reducer). However, componentDidUpdate is not called if this ordering is the first thing I do after page load. If I take other actions that update the props/state, and then attempt to re-order, the ordering works fine and componentDidUpdate is called.
In the former situation, where ordering is the first thing I try to do after page load/refresh, the render function is hit (with the properly re-ordered props) but the DOM never updates, presumably because redux does not recognize a state change?
It just seems like some other props change (even if unrelated to arrays) "wakes up" redux or connect and allows it to recognize changes.
Here's where I copy, dispatch and then release the updated state in the reducer:
in actionCreator method:
...
const newProjects = getState().tracker.projects.slice().map(p => {
if (p.innerArray) {
const from = p.innerArray[draggedId-1];
const to = p.innerArray[droppedId-1];
p.innerArray[draggedId - 1] = { ...to, order: draggedId };
p.innerArray[droppedId - 1] = { ...from, order: droppedId };
}
return p;
})
dispatch({ type: 'RECEIVE_FIELD_UPDATE', projects: newProjects, message: "Sorted" });
reducer:
case 'RECEIVE_FIELD_UPDATE':
return {
...state,
projects: action.projects,
message: action.message
}
As you can see, I'm not mutating state in the reducer or doing other anti-pattern things (as far as I'm aware).
FWIW, the "Sorted" value on message property seems to get through whether or not componentDidUpdate is hit (I have a modal that pops up with the message to confirm).
Any pointers or related documentation would be helpful; I've been reading all I can find, including other StackOverflow posts, but to no avail.
UPDATE
Solved the issue, it did have to do with redux not diff-ing the properties but not because of what I expected, read below if interested.
Figured this out, and per usual I was looking in the wrong place.
The issue was that my conditional check on page load to prevent re-loading of already stored data was preventing any dispatches from firing. When I looked in the reducer, the sub-arrays of the projects were identical after dragging, because the state had not been previously connected yet. I thought a hard refresh would cause a full re-load but apparently I need to read more about how redux stores data and how to refresh that.
Anyways, fixing this condition so that I get new data on page load fixed the issue and componentDidUpdate is now being triggered consistently.
When updating state in a reducer in an array format, I've had to spread state again in order for updates to happen and keep from mutating.
case 'RECEIVE_FIELD_UPDATE':
return {
...state,
projects: ...state, ...action.projects,
message: action.message
}
or...
case 'RECEIVE_FIELD_UPDATE':
return {
...state,
projects: [...state, ...action.projects],
message: action.message
}
Depending on the format.
Hope that helps.
What's an efficient/elegant way to access nested data from branches of the state that are only available part of the time?
I'm new to React and this problem keeps coming up. I have an ok grasp on Redux and using middleware like Thunk & Saga for the API stuff, but I still don't understand the proper way to write components so that they're not trying to grab non-existent state.
For example, when loading a user's profile photo into a header after the user signs in, the URL will be in the redux store as state.user.userData.photos.primary ...if I try to access that location when the user hasn't signed in, userData is still undefined and React will throw an error. So my solution has been to write assignments like:
let profilePhoto = props.user.userData ? props.user.userData.photos.primary : '';
... which seems cumbersome and inefficient, especially since it requires mapping the entire userData object in mapStateToProps only for the sake of accessing a tiny chunk of data.
I wish I could target it earlier. For example, in mapStateToProps:
profilePhoto : state.user.userData.photos.primary || '';
...but that results in "can't access 'photos' of undefined". So what's an efficient/elegant way to access nested data from branches of the state that are only available part of the time?
In your mapStateToProps access your state like this:
profilePhoto : state.user.userData ? state.user.userData.photos.primary : ''
and then use profilePhoto as props. This will not throw an error of undefined.
In my current application I initialize the redux state with all the data tree that I will use; so that I will unlikely end up with undefineds.
So a solution would be:
const initialState = {
userData: {
photos: {
primary: ""
}
}
}
This might look ugly in the first glance but it helps me keep track of what my data will look like, and can be simplified by defining parts of the object individually.
An alternative could be keeping a flag in the redux state like isUserPrimaryPhotoDefined which you would set to true when you are setting the primary variable; so you can use the flag in the ternary. However that might not guarantee getting rid of the possible errors completely.
With ES6 Destructuring Assignment, you can access nested keys from props in a cleaner way.
const { user: { userData: { photos: { primary = ''} = {}} = {}} = {}} = props;
console.log(primary); // prints primary value or '' if not defined
In my reducer i have this state:
const initialState = fromJS({
resources: false,
testval: [0, 0, 0, 0],})
I want to append or remove array values for testval when my action is fired. I have immutable, and immutability-helper imported but im struggling with the syntax to get this to work.
For example this isnt working - the state isnt changing:
return update(state, {
testval: {
$set: 'test'
}
});
It looks like you might be mixing up two different toolsets. Immutable.js is a specialized set of data structures, while the Immutability-Helper library expects to work on plain JS objects.
You might want to read through the Immutable Update Patterns section of the Redux docs, which discusses ways to properly do immutable updates on plain JS data, as well as the related links in the Prerequisite Concepts docs page.
Ok i figured this out using Immutable.
Add to array:
return state.updateIn(['showtask'], arr => arr.push(action.userid))
Remove from array:
return state.updateIn(['showtask'], arr => arr.splice(arr.indexOf(action.userid),1))
its because you are updating it wrong way,
1st thing is your array will be converted to List object, so that need to be updated in list style .
in below example 1 is the index i am updating, you can use any dynamic variable for this, also item is the current value at that index. if that is object(in some other use case) you can modify the object and return it.
const initialState = Immutable.fromJS({
resources: false,
testval: [0, 0, 0, 0]});
//replace initial state with state, i just posted example code
initialState = initialState.set("testval",initialState.get("testval").update(1,(item)=>{
return 2;
}));
console.log(initialState.get("testval"))
for more detailed example you can also check this post https://onl9class.com/using-immutable-js/
I am in the early development stage of a React+Redux game and have followed Redux best practices: pure reducer, presentational/container component separation, using getState() only in Reducer (as opposed to in action creator) etc. The app seems to be working as expected but when I try to reverse an action using Time Travel, even though the state property map[][] and it's computed connected component prop change as expected, the result doesn't get reflected on the UI properly (specifically the player position on the map doesn't follow what state dictates). When I inspect the state changes I can see that all necessary changes are correctly taking place between different states. Here is my reducer:
const gridReducer = (state, action) => {
if (typeof state === 'undefined'){
let dungeon = new Dungeon();
dungeon.generate();
return {
boardWidth: Math.floor(((70/100) * window.innerWidth) / 20),
boardHeight: Math.floor(((70/100) * window.innerHeight) / 20),
map: dungeon.map,
position: dungeon.playerPosition
}
}
switch (action.type) {
case 'GRID_RESIZE':
return {...state,
boardWidth: action.newBoardWidth,
boardHeight: action.newBoardHeight
}
//This is where I have the issue, map correctly changes both when interacting with the game and when reversing using time travel however the UI fails to update (only in reverse)!
case 'MOVE':
let dungeonObj = new Dungeon(state.map.slice(), {...state.position});
if (dungeonObj.movePlayer(action.direction)) {
return {...state,
position: dungeonObj.playerPosition,
map: dungeonObj.map
}
} else return state;
default:
return state;
}
}
Here is the complete code if you want to take a look! The app currently only supports moving the player in the dungeon by pressing arrow keys and the view is supposed to always be centeral based on the position of the player (player fails to move back when using time travel)
http://s.codepen.io/sabahang/debug/GjrPNQ
PS: Dungeon.generate does use Math.Random but I'm only using this function in initialState and for dispatched actions I'm only making a shallow copy of the generated map by sending the current state to Dungeon constructor and use its other methods (eg. movePlayer)
Found the culprit. It's not Redux's fault at all, it's about the way React works! If you are new to React and you haven't fallen into this trap yet wait for it!
It has to do with the fact that most of the conventional ways of copying a deeply nested object which is needed in Redux to implement a pure Reducer is in fact making a shallow copy of the objects and properties' memory references are still pointing to the original State. React updates the UI based on a deep comparison of the old state and the new one and when some of the references are the same it fails to update the UI properly. Here I have a 2 dimensional array map[][] which is an object and although I'm using ES6 spread operator to avoid modifying the original state because a shadow copy is being made, deeply nested indexes of the original map[][] are being modified. One solution would be to use `Array.map()' to create a completely new object but I ended up using immutablejs and it fixed my problem with the time travel slider.
This is a highly recommended reference if you don't want to spend weeks chasing similar bugs in complicated apps: http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html
and there are tons of immutability helpers to help based on your specific need:
https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md#immutable-update-utilities
This one also looks interesting for Redux only:
https://github.com/indexiatech/redux-immutablejs
This question is potentially a duplicate of the following:
React-redux store updates but React does not