Bear with me here as this question pertains to my first test app using either React, Redux or react-redux. Docs have gotten me far and I have a mock banking app that mostly works. My state object looks roughly like this:
{
activePageId: "checking",
accounts: [
checking: {
balance: 123,
transactions: [
{date, amount, description, balance}
]
}
]
}
I have just two actions:
1. CHANGE_HASH (as in url hash). This action always works as expected and all the reducer does is update the state.activePageId (yes, I'm cloning the state object and not modifying it). After the action, I can see the state has changed in the Redux store and I can see that React has updated.
function changeHash(id) {
return {
type: "CHANGE_HASH",
id: id
}
}
2. ADD_TRANSACTION (form submission). This action never updates React, but it always updates the Redux store. The reducer for this action is updating state.accounts[0].balance and it's adding a transaction object to the array state.accounts[0].transactions. I don't receive any errors, React just doesn't update. HOWEVER, if I dispatch a CHANGE_HASH action React will catch up and display all of the ADD_TRANSACTION state updates properly.
function addTransaction(transaction, balance, account) {
return {
type: "ADD_TRANSACTION",
payload: {
transaction: transaction,
balance: balance,
account: account
}
}
}
My reducer...
function bankApp(state, action) {
switch(action.type) {
case "CHANGE_HASH":
return Object.assign({}, state, {
activePageId: action.id
});
case "ADD_TRANSACTION":
// get a ref to the account
for (var i = 0; i < state.accounts.length; i++) {
if (state.accounts[i].name == action.payload.account) {
var accountIndex = i;
break;
}
}
// is something wrong?
if (accountIndex == undefined) {
console.error("could not determine account for transaction");
return state;
}
// clone the state
var newState = Object.assign({}, state);
// add the new transaction
newState.accounts[accountIndex].transactions.unshift(action.payload.transaction);
// update account balance
newState.accounts[accountIndex].balance = action.payload.balance;
return newState;
default:
return state;
}
My mapStateToProps
function select(state) {
return state;
}
What am I missing here? I'm under the impression that React is supposed to update as the Redux storeis updated.
Github repo:
Deployment bank demo
p.s. I lied about not having any errors. I do have a number of warnings
""Warning: Each child in an array or iterator should have a unique "key" prop..."
I'm already giving them a key prop set to it's index. I doubt that has anything to do with my issue though.
The problem is in this piece of code:
// clone the state
var newState = Object.assign({}, state);
// add the new transaction
newState.accounts[accountIndex].transactions.unshift(action.payload.transaction);
// update account balance
newState.accounts[accountIndex].balance = action.payload.balance;
Cloning the state object doesn't mean you can mutate the objects it is referring to. I suggest you to read more about immutability because this isn't how it works.
This problem and solution to it are described in detail in Redux “Troubleshooting” docs so I suggest you to read them.
https://redux.js.org/troubleshooting
I also suggest you to take a look at Shopping Card example in Flux Comparison for Redux because it shows how to update nested objects without mutating them in a similar way to what you are asking.
https://github.com/voronianski/flux-comparison/tree/master/redux
Related
I am trying for few hours but can't figure out why my state is not called after adding an array of custom object.
// In my component...
const myRemoteArray = getRemoteArray() // Is working
props.addAdItems(myRemoteArray) // Calls **1 via component.props
/// ...
const mapDispatchToProps = (dispatch) => {
return {
addAdItems: (items) => { // **1
// Items contains my array of objects
dispatch(addAdItems(items)) // Calls **2
},
}
}
// My action
export const addAdItems = (items) => { // **2
// Items contains my array of objects
return { // Calls **3
type: AD_ITEMS,
adItems: items,
}
}
const productsReducer = (state = initialState, action) => {
switch (action.type) { // **3
case AD_ITEMS:
// Is working!
// action.adItems contains my array!
const _state = {
...state,
adItems: action.adItems, // Here is the issue, I am not sure how to add my NEW array to existing state and update it.
// Like that: ??? "adItems: ...action.adItems" or adItems: [action.adItems]
}
// The new state contains my Array!!!
return _state
default:
return state
}
}
// In my component... !!!!
// THIS IS NOT CALLED or it is called with empty array from initialState!!!
const mapStateToProps = (state) => {
return {
updatedItem: state.changedItem,
adItems: state.adItems,
}
}
It seems to me that Redux is having a problem with my array containing the following data. Has Redux issues with my class methods?
class Ad {
constructor(
id,
isPublished
) {
this.id = id
this.isPublished = isPublished
}
someMessage = () => { return "Help me!" }
needHelp = () => { return true }
}
My Redux is working already with other calls, data, and objects, which means my createStore and all other stuff is correct.
PS: I don't have multiple stores.
UPDATE
Now my mapDispatchToProps is called with current array but is not persisting.
UPDATE 2
If I save my file and force to refresh the App, the props.adItems contains my loaded array, but if I want to access props.adItems at runtime (e.g. on FlatList refresh) it is empty array again!
Why?
Should I store my array in a useState property after it has changes via useEffect?
You were pretty close in the comments you added in the reducer, but neither of them were 100% accurate.
For Redux to notice that your array has changed, you need the property adItems of your new state to return an entirely new array. You can do it like this:
adItems: [...action.adItems]
With this code you'll be creating a new array, and then adding a copy of the items of the old one into it.
The reason why your current implementation (adItems: action.adItems) is not working is that action.adItems is actually a reference to an array in memory. Even though the array contents have changed, the value of action.adItems is still the same, a pointer to where the array is currently stored. This is the reason why your store is not being updated: as Redux does not check the values of the array itself but the reference to where the array is stored, the new state you're returning is exactly the same, so Redux is not aware of any changes.
As LonelyPrincess says, I was making this issue elsewhere, if you doing that xArray = yArra it means call by reference and not by value.
For example, could I iterate over Vuex data in a Vue file and choose the data needing updating, then pass the found data to an action, which commits it and then the mutation only makes the update?
The reason I'm unsure about it is because the typical format of a Vuex mutation contains the parameter for 'state', so I assume it needs to be used, and the only way to do that is either by doing all the looping inside the mutation, or to pass indexes to it to more quickly find the exact fields needing changing.
For who asked, a code example:
someVueFile.vue
computed: {
...mapState({
arrayOfObjects: (state) => state.someVuexStore.arrayOfObjects
}),
},
methods: {
myUpdateMethod() {
let toBePassedForUpdate = null;
let newFieldState = "oneValue";
this.arrayOfObjects.forEach((myObject) => {
if (myObject.someDataField !== "oneValue") {
toBePassedForUpdate = myObject.someDataField;
}
})
if (toBePassedForUpdate) {
let passObject = {
updateThis: toBePassedForUpdate,
newFieldState: newFieldState
}
this.$store.dispatch("updateMyObjectField", passObject)
}
}
}
someVuexStore.js
const state = {
arrayOfObjects: [],
/* contains some object such as:
myCoolObject: {
someDataField: "otherValue"
}
*/
}
const mutations = {
updateMyObjectField(state, data) {
data.updateThis = data.newFieldState;
}
}
const actions = {
updateMyObjectField(state, data) {
state.commit("updateMyObjectField", data);
}
}
Yes, it's alright to mutate state passed in through the payload argument rather than state. Vuex doesn't bother to distinguish between the two. In either case, it's the same state, and neither option detracts from the purposes of using mutations.
To feel more sure of that, you can ask what are the purposes of mutations and of enforcing their use. The answer is to keep a centralized, trackable location for concretely defined changes to state.
To illustrate this is a good thing, imagine an app with 1000 components, each one changing state locally, outside of a mutation, and in different ways. This could be a nightmare to debug or comprehend as a 3rd party, because you don't know how or where state changes.
So mutations enforce how and a centralized where. Neither of these are damaged by only using the payload argument in a mutation.
I would do all of the logic from one action, you can desctructured the context object in the action signature like so :
actions: {
myAction ({ state, commit, getters, dispacth } ,anyOtherParameter) {
let myVar = getters.myGetter//use a getter to get your data
//execute logic
commit('myCommit', myVar)//commit the change
}
}
If you need to do the logic in your component you can easily extract the getter and the logic from the action.
I have a table like
When you edit the quantity using this onChange
onChange={this.handleInputChange.bind(null, cellInfo)}
I run the below code
handleInputChange = (cellInfo, event) => {
let data = { ...this.props.Data };
data[cellInfo.index][cellInfo.column.id] = parseInt(event.target.value);
this.props.APISummaryData(data);
};
Goal being first get the data in the store, then reflect the value you changed and then update it with action this.props.APISummaryData(data); and this.props.APISummaryData({ ...data }); both give same State mutation error.
Here's the reducer
case types.API_SUMMARY_DATA:
return {
...state,
Summary: {
...state.Summary,
Data: action.Summary
}
};
If I manually dispatch an action within Redux inside DevTools doing
{
type: 'API_SUMMARY_DATA',
Summary: [
{
cusip: '019I',
quantity: 55,
}
]
}
This is the action
export const APISummaryData = Summary => ({ type: types.API_SUMMARY_DATA, Summary });
I don't get any error and data gets updated. I am so puzzled where in this scheme I mutate the state?
Note: it is possible I am not sharing some code that's important to take a look here, so please let me know and I'll share it.
exact error
I assume that you're using configureStore() from Redux Starter Kit, which sets up a mutation checking middleware by default. Good! This means that the mutation checker is doing its job correctly.
These lines right here are mutating:
let data = { ...this.props.Data };
data[cellInfo.index][cellInfo.column.id] = parseInt(event.target.value);
That's because the {...} object spread operator does a shallow copy, not a deep copy. This is a very common mistake.
I personally would recommend dispatching an action that looks like:
{type: "API_SUMMARY_DATA", payload: {index, columnId, inputValue}}
and then use the reducer to do all the updating.
Also, if you are using Redux Starter Kit, you can use our createReducer() function to write "mutative" code in the reducer that actually does immutable updates.
Using React-redux here and having a bit of an issue, that some of you might help with.
The user can create 'Jobs' (posts) and also remove them. Adding them is no issue and the reducer returns what is expected. However, once I delete a job from the (firebase) database I trigger a new fetch for the current jobs, but the reducer still returns the old jobs. Am I missing something?
Before deleting, this is how the jobs objects looks like:
activeJobs= {
-KrkPPy4ibSraKG-O49S: {
title: 'Help',
location: 'etc,
...
},
-KrkPPy4ibSraKG-O49S: {
title: 'Help',
location: 'etc,
...
} and so on
}
When I delete them all I get this {} back from the server. Expected.
What is not expected is that my reducer still returns the old jobs and my components do not re-render.
I dispatch an action after fetching the jobs:
firebase.database().ref(`/jobs/activeJobs/${currentUser.uid}`)
.on('value', snapshot => {
console.log('new activeJobs ===', snapshot.val());
dispatch({
type: FETCH_JOBS_SUCCESS,
payload: snapshot.val()
});
});
snapshot.val() does contain the new updated jobs.
Then here is the reducer that handles the action:
switch (action.type) {
case FETCH_JOBS_SUCCESS:
// ...state contains the OLD jobs and action.payload contains {}. Why is is not overriding it the old jobs?
return { ...state, ...action.payload };
default:
return state;
}
Why is my reducer failing?
The { ...state, ...action.payload } syntax actually mean : build a new object by taking every prop of state and adding every props of action.payload. In your case, you just get a new object that is similar to state, since ...action.payload is an empty object.
Change your action to
return Object.assign({}, state, {activeJobs : action.payload});
Let's say i have a rootreducer like below.
const rootR = combineReducers({
topics,
...
});
the topics reducer
function topics(state = { topics=[], error: null}, action){
switch (action.type){
case types.FETCH_TOPICS_SUCCESSFULLY:
const { topics } = action;
return {
...state,
topics,
};
default:
return state;
}
};
And when i fire the related action i get my state with repeatable properties state.topics.topics instead of state.topics
Is there any way to avoid this repeating (topics.topics)?
Thanks in advance
Looking at the initialState of your topics reducer, the state object accessible to topics reducer has this structure:
{
topics: [],
error: null
}
So when you combineReducers like this:
const rootR = combineReducers({
topics,
anotherReducer,
someOtherReducer.
// ...
});
resulting global app state is going to look like this:
{
topics: {
topics: [],
error: null
},
anotherReducer: {
// ...
},
someOtherReducer: {
// ...
},
// ...
}
So if you want to access topics array from global state, you need to do state.topics.topics.
You have two things under state.topics, an array of topics and error.
Hence let's rename second topics key to items to avoid confusion.
(it is unavoidable to have a second key to store the array because you also want error)
thus we have:
state.topics = {
items: [],
error: null,
}
Instead of state.topics.topics, now we access state.topics.items
To achieve this, initialstate passed to topics reducer has to be:
function topics(state = { items = [], error: null }, action){
//...
}
Now inside the reducer FETCH_TOPICS_SUCCESSFULLY, we want to append an array action.topics to items, like this (without mutating our current state):
case types.FETCH_TOPICS_SUCCESSFULLY:
const { topics } = action;
return {
...state,
items: [
...state.items,
...topics
],
};
#markerikson is right, the state variable passed in the function is actually topics once FETCH_TOPICS_SUCCESSFULLY is called, so it's better to do return topics there.
But given your condition, instead of return {...state, topics} or return topics, you can also do return Object.assign({}, state, topics). This will create a new object with all properties from previous state and topics merged together.
You're double-nesting things. The topics reducer will only see the "topics" slice of state. So, instead of returning {...state, topics}, just do return topics.
update
Your edit to the question changes the situation considerably.
Originally, you had:
function topics(state = {}, action){
Now, you have:
function topics(state = { topics=[], error: null}, action){
I'll admit I'm a bit confused at this point as to what your desired state structure actually should be.
Looking at your original definition, it seemed like you were misunderstanding how combineReducers works, and redundantly trying to return a new object that contained a field/slice named "topics". Now, it looks like the root-level "topics" slice itself has a field named "topics" as well.
Are topics and error supposed to be at the root of your state tree? Or, are they both really supposed to be part of the top-level "topics" slice? If that's really what you want, then you've defined the state tree as needing to be topics.topics.
Also, to answer #free-soul: no, in the original example, return topics would not mutate state, because it's just returning whatever was in the action. Even if the action.topic field was literally the same array that used to be in the state, the result would just be a no-op.