Pattern for updating multiple parts of Redux state - javascript

The shape of my Redux state looks like this:
{
user: {
id: 123,
items: [1, 2]
},
items: {
1: {
...
},
2: {
...
}
}
}
Using combineReducers I have 2 sets of reducers. Each act on one of the root keys of the state. i.e. one manages the user key and the other the items key.
If I want to add an item I can call 2 reducers, the first will add a new object to the items and the second will add the id to the user.items array.
This has a bad code smell. I feel that there should be a way to atomically reduce the state of both objects at the same time. i.e. in addition to the sub-reducers have a root reducer that acts on the root object. Is this possible?

I think what you're doing is actually correct!
When dispatching an action, starting from the root-reducer, every "sub-reducer" will be called, passing the corresponding "sub-state" and action to the next layer of sub-reducers. You might think that this is not a good pattern since every "sub-reducer" gets called and propagates all the way down to every single leaf node of the state tree, but this is actually not the case!
If the action is defined in the switch case, the "sub-reducer" will only change the "sub-state" part it owns, and maybe passes the action to the next layer, but if the action isn't defined in the "sub-reducer", it will do nothing and return the current "sub-state", which stops the propagation.
Let's see an example with a more complex state tree!
Say you use redux-simple-router, and I extended your case to be more complex (having data of multiple users), then your state tree might look something like this:
{
currentUser: {
loggedIn: true,
id: 123,
},
entities: {
users: {
123: {
id: 123,
items: [1, 2]
},
456: {
id: 456,
items: [...]
}
},
items: {
1: {
...
},
2: {
...
}
}
},
routing: {
changeId: 3,
path: "/",
state: undefined,
replace:false
}
}
As you can see already, there are nested layers in the state tree, and to deal with this we use reducer composition, and the concept is to use combineReducer() for every layer in the state tree.
So your reducer should look something like this:
(To illustrate the layer by layer concept, this is outside-in, so the order is backwards)
first layer:
import { routeReducer } from 'redux-simple-router'
function currentUserReducer(state = {}, action) {
switch (action.type) {...}
}
const rootReducer = combineReducers({
currentUser: currentUserReducer,
entities: entitiesReducer, // from the second layer
routing: routeReducer // from 'redux-simple-router'
})
second layer (the entities part):
function usersReducer(state = {}, action) {
switch (action.type) {
case ADD_ITEM:
case TYPE_TWO:
case TYPE_TREE:
return Object.assign({}, state, {
// you can think of this as passing it to the "third layer"
[action.userId]: itemsInUserReducer(state[action.userId], action)
})
case TYPE_FOUR:
return ...
default:
return state
}
}
function itemsReducer(...) {...}
const entitiesReducer = combineReducers({
users: usersReducer,
items: itemsReducer
})
third layer (entities.users.items):
/**
* Note: only ADD_ITEM, TYPE_TWO, TYPE_TREE will be called here,
* no other types will propagate to this reducer
*/
function itemsInUserReducer(state = {}, action) {
switch (action.type) {
case ADD_ITEM:
return Object.assign({}, state, {
items: state.items.concat([action.itemId])
// or items: [...state.items, action.itemId]
})
case TYPE_TWO:
return DO_SOMETHING
case TYPE_TREE:
return DO_SOMETHING_ELSE
default:
state:
}
}
when an action dispatches
redux will call every sub-reducer from the rootReducer,
passing:
currentUser: {...} sub-state and the whole action to currentUserReducer
entities: {users: {...}, items: {...}} and action to entitiesReducer
routing: {...} and action to routeReducer
and...
entitiesReducer will pass users: {...} and action to usersReducer,
and items: {...} and action to itemsReducer
why is this good?
So you mentioned is there a way to have the root reducer handling different parts of the state, instead of passing them to separate sub-reducers. But if you don't use reducer composition and write a huge reducer to handle every part of the state, or you simply nest you state into a deeply nested tree, then as your app gets more complicated (say every user has a [friends] array, or items can have [tags], etc), it will be insanely complicated if not impossible to figure out every case.
Furthermore, splitting reducers makes your app extremely flexible, you just have to add any case TYPE_NAME to a reducer to react to that action (as long as your parent reducer passes it down).
For example if you want to track if the user visits some route, just add the case UPDATE_PATH to your reducer switch!

Related

Redux State Mutation When Dispatch an 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.

Can I use condition in my action reducer?

Basically, in our case, we need to either get an alerts list that shows the first few items (mounting it first time in the DOM) or show the initial list + the next list (clicking a load more button).
Hence we needed to do this condition in our GET_ALERTS action:
case "GET_ALERTS":
if (action.initialList) {
newState.list = [...newState.list, action.res.data.list];
} else {
newState.list = newState.list.concat(
action.res.data.list
);
}
And when we call the action reducer in our Alerts component, we need to indicate whether initialList is true or false.
E.g.
componentDidMount() {
this.props.getAlerts(pageNum, true);
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum, true);
}
Anyway in such a case, is it fine to use conditional statement in the reducer?
I am against this idea. The reducer has a single responsibility: update Redux state according to the action.
Here are three ways to slove this:
easy way - initialize your list in Redux state to empty list
if you set the list in state to empty list ([]) then it's much simpler.
You can basically just change your reducer to this:
case "GET_ALERTS":
return {...state, list: [...state.list, action.res.data.list]
This will make sure that even if you have get initial list or more items to add to the list, they will be appended. No need to add any logic - which is awesome IMHO.
redux-thunk and separating type into two different types
create two actions: GET_INIT_ALERTS and GET_MORE_ALERTS.
switch(action.type) {
case "GET_INIT_ALERTS":
return {...state, list: action.res.data.list }
case "GET_MORE_ALERTS":
return {...state, list: [...state.list, ...action.res.data.list]}
case "CHECK_READ_ALERTS":
return {...state, read: [...state.read, ...action.res.data.list]}
}
In the component I will have:
componentDidMount() {
this.props.getInitAlerts();
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum);
}
In alerts action with the help of redux-thunk:
export const getAlerts = (pageNum : number) => (dispatch) => {
return apiAction(`/alerts/${pageNum}`, 'GET').then(res => dispatch({type: "GET_MORE_ALERTS", res});
}
export const getInitAlerts = () => (dispatch) => {
return apiAction('/alerts/1', 'GET').then(res => dispatch({type: "GET_INIT_ALERTS", res});
}
I guess you update pageNum after readMore or componentDidMount. Of course you can save that state in Redux and map it back to props and just increment it when calling the getAlerts action.
write your own middleware
Another way to do this is to write an ad-hoc/feature middleware to concat new data to a list.
const concatLists = store => next => action => {
let newAction = action
if (action.type.includes("GET") && action.initialList) {
newAction = {...action, concatList: action.res.data.list}
} else if (action.type.includes("GET") {
newAction = {...action, concatList: [...state[action.key].list, action.res.data.list]}
}
return next(newAction);
}
And change your reducer to simply push concatList to the state:
case "GET_ALERTS":
return {...state, list: action.concatList}
In addition, you will have to change your action to include key (in this case the key will be set to alert (or the name of the key where you store the alert state in redux) and initialList to determine whether to concat or not.
BTW, it's a good practice to put these two under the meta key.
{
type: "GET_ALERT",
meta: {
initialList: true,
key: "alert",
},
res: {...}
}
I hope this helps.
I would suggest you to have following set of actions:
ALERTS/INIT - loads initial list
ALERTS/LOAD_MORE - loads next page and then increments pageNo, so next call will know how many pages are loaded
ALERTS/MARK_ALL_AS_READ - does server call and reinitializes list
The store structure
{
list: [],
currentPage: 0
}
And component code should not track pageNum
componentDidMount() {
this.props.initAlerts();
}
markAllAsRead() {
this.props.markAllAsRead();
}
readMore() {
this.props.loadMore();
}

Avoiding repeating state names

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.

redux how to get or store computed values

How should I implement in redux computed values based on it's current state?
I have this for an example a sessionState
const defaultState = {
ui: {
loading: false
}, metadata: { },
data: {
id: null
}
}
export default function sessionReducer(state = defaultState, action) {
switch(action.type) {
case STORE_DATA:
return _.merge({}, state, action.data);
case PURGE_DATA:
return defaultState;
default:
return state;
}
}
Say for example I want to get if the session is logged in, I usually do right now is sessionState.data.id to check it, but I want to know how I can do sessionState.loggedIn or something?
Can this do?
const defaultState = {
ui: {
loading: false
}, metadata: { },
data: {
id: null
},
loggedIn: () => {
this.data.id
}
}
or something (that's not tested, just threw that in). In this example it looks simple to just write .data.id but maybe when it comes to computations already, it's not good to write the same computations on different files.
Adding methods to state object is a bad idea. State should be plain objects. Keep in mind, that some libraries serialize the app state on every mutation.
You can create computing functions outside the state object. They can receive state as an argument. Why should they be state's methods?
const defaultState = {
ui: {
loading: false
}, metadata: { },
data: {
id: null
}
}
const loggedIn = (state) => {
//your logic here
}
In your particular example, the calculated result is incredibly simple and fast to calculate so you can calculate it on demand each time. This makes it easy to define a function somewhere to calculate it like #Lazarev suggested.
However, if your calculations become more complicated and time consuming, you'll want to store the result somewhere so you can reuse it. Putting this data in the state is not a good idea because it denormalizes the state.
Luckily, since the state is immutable, you can write a simple pure function to calculate a result and then you can use memoization to cache the result:
const isLoggedIn = memoize(state => state.login.userName && state.login.token && isValidToken (state.login.token));
Lastly, if you want to use methods on your store state, you can use Redux Schema (disclaimer: I wrote it):
const storeType = reduxSchema.type({
ui: {
loading: Boolean
}, metadata: { },
data: {
id: reduxSchema.optional(String)
},
loggedIn() {
return Boolean(this.data.id);
}
});

React-redux store updates but React does not

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

Categories

Resources