I am currently trying to wrap my brain around redux and I currently have a problem understanding, how to handle dependent state in redux.
As an example think of a spreadsheet application:
In Cell A1 and A2 the user is entering values.
Cell A3 now has the following dependent state (=Sum(A1;A2))
So now, when the user enters "2" into A1 => we send an "UpdateCellAction: A1=2"
and then, he enters "4" into A2 => we send an "UpdateCellAction: A2=4"
But because of these changes, my formula in A3 has to react and also modify the state object by viewing the sum of 2 and 4, which is 6
How is something like this done in Redux?
And, what if there was another Cell B22 (whatever) which calculated another value based on A3, A2 and A1? (that state would then depend on A1, A2 and A3)
This is a common use case. What you're looking for is the react/reselect library:
From the reselect github project page:
Selectors can compute derived data, allowing Redux to store the minimal possible state.
Selectors are efficient. A selector is not recomputed unless one of its arguments change.
Selectors are composable. They can be used as input to other selectors.
Your question isn't clear if you consider each cell to be an individual component or not.
If each cell is an individual component:
You would use Redux to dispatch an action every time each cell is edited and keep that value under a unique key in the Redux store. You would also use mapStateToProps to get all the values of the other components you want to calculate locally.
...
/**
* Redux mappings
*/
const mapStateToProps = (state) => {
return {
cellA1: state.cells.cellA1,
cellA2: state.cells.cellA2,
};
};
const mapDispatchToProps = (dispatch) => {
return {
updateCell: (newCellValue) => {
dispatch(Actions.updateCell(newCellValue));
},
};
};
const ConnectedCellComponent = connect(
mapStateToProps,
mapDispatchToProps,
)(CellComponent);
If the spreadsheet as a whole is a component:
You would store all the cell data in a local state object, then whenever a cell updates you would call setState() and render() would get called automatically and all the cells would recalculate appropriately.
Related
I'm becoming a bit confused about how to update state in NgRx reducer, here is the question.
Say I have a state
{
xxx: num
yyy: classtpe
...
data: Data[]
}
If I have an action to add a Data item to the list.
I know I can't call data.push() because that just update the array but data pointing to same array so in the reducer I have
state.data = cloneDeep(state.data)
state.data.push(newdata)
so state.data now is different from previous one because they are 2 individual arrays.
my question is, can I return state directly now? If yes then the old and new states point to same variable, they have exactally same members except data.
The other way is, return a brand new state like
return Object.assign({}, state, {
data: [...state.data, newdata]
})
or
const newstate = cloneDeep(state)
newstate.data.push(new data)
return newstate
this way new and old state are totally different.
I think it's actually related to how difference is checked in NgRx? first way they need to go through each memeber, if any member is different then the state is differnet?
2nd way the 2 states are different, but all the memebers need to be checked to see if contents are same.
I would recommend you to use ngrx-immer, https://github.com/timdeschryver/ngrx-immer
It's an Immer wrapper around ngrx reducers so you can update all of the state mutably, and it will do the rest for you. Immer is also used in the redux-toolkit to make it simple to update state.
In the Redux Style Guide, it is strongly recommended to Put as Much Logic as Possible in Reducers:
Wherever possible, try to put as much of the logic for calculating a
new state into the appropriate reducer, rather than in the code that
prepares and dispatches the action (like a click handler).
What I'm not sure of is, if thunks are also considered to be "the code" of some sort. Besides, we've also been (mis?)using thunks to grab data from other slices of state.
Hypothetically simplified code snippet of such thunk:
const addX = x => (dispatch, getState) => {
const { data, view } = getState();
const { y } = view; // <-- here accessing data from `view` state.
const yy = doSomeLogicWith(y);
const z = doSomeMoreLogicWith(yy);
dispatch({ type: 'data/xAdded', payload: { x, z } });
};
Is this actually considered to be an anti-pattern in Redux? If so, what are the cons of doing this?
Yes, a thunk would qualify as "the code that dispatches the action" for this case. So, what the rule is recommending here is that if possible, the action would just contain y, and the function calls to doSomeLogicWith(y) and doSomeMoreLogicWith(yy) would ideally exist within the reducer instead.
Having said that, it's totally fine for a thunk to extract pieces of data from the state and include that in the action, and it's not wrong for a thunk to do some pre-processing of data before dispatching the action.
The style guide rule is just saying that, given a choice between running a particular piece of logic in a reducer or outside a reducer, prefer to do it in the reducer if at all possible.
Given the following (and assuming we cannot change the state's structure):
StoreModule.forRoot({
a: aReducer,
b: {
b1: b1Reducer,
b2: b2Reducer
}
});
and b1Reducer is dependent on the value of a (for example because it contains something like user info).
What is the most idiomatic way to access (read-only) a in b1Reducer?
The solution I came up with is using #ngrx/effects, dispatch another action with a that can be used in the reducer:
#Effect()
augmentAction$ = this.action$
.ofType(Actions.Action1)
.withLatestFrom(this.store$)
.switchMap(([action, state]:[Action, AppState]) => {
const a = state.a;
return [new Actions.Action2(a)];
});
This works, but it becomes hard to manage if almost every action needs to be redispatched if a is used in many reducers. Is there a better way to handle this?
I am working on a Redux project with a bunch of reducers that are combined
const rootReducer = combineReducers({
reducer1,
reducer2,
...
});
The reducers each update a branch of the store
sampleStore = {
reducer1: {a1: 7, b1: 5, c1: 6, ...},
reducer2: {a2: 3, b2: 2, c2: 9, ...},
reducer3: {a3: 1, b3: 4, c3: 2, ...},
...
}
Now I am trying to add logic that requires access to both branches of the store without restructuring the entire project. For instance, I want a reducer with the ability to perform: set c3 = f(a1, a2).
At the moment I am trying to use reselect but I am getting lost. In reducer1/selector.js I have
export const derivedVariableSelector = createSelector(
a1Selector,
a2Selector,
(a1, a2) => a1 + a2
);
And I am trying to create a reducer
function setC3(state) {
return Object.assign({}, state, {
c3: state.c3 + derivedVariableSelector(state)
});
}
However, when setC3 is called, derivedVariableSelector will never receive enough of the store to do its job. Either it will receive the branch containing a1 or the branch containing a2, but I don't know of a way to supply both. Is this possible?
The short answer is "No", so long as you're using combineReducers to combine all your reducers.
The longer answer is taken from the redux documentation:
The combineReducers utility included with Redux is very useful, but is
deliberately limited to handle a single common use case: updating a
state tree that is a plain Javascript object, by delegating the work
of updating each slice of state to a specific slice reducer. It does
not handle other use cases, such as a state tree made up of
Immutable.js Maps, trying to pass other portions of the state tree as
an additional argument to a slice reducer, or performing "ordering" of
slice reducer calls. It also does not care how a given slice reducer
does its work.
The common question, then, is "How can I use combineReducers to handle
these other use cases?". The answer to that is simply: "you don't -
you probably need to use something else". Once you go past the core
use case for combineReducers, it's time to use more "custom" reducer
logic, whether it be specific logic for a one-off use case, or a
reusable function that could be widely shared.
On the same page of the docs are some suggestions for how the create reducers that share more of the state but it's going to be a bit more manual than comibineReducers is.
I haven't tried anything like this myself before, but following examples in the docs, something like this might work for you:
import { combineReducers } from 'redux'
import reduceReducers from 'reduce-reducers'
const reducer1 = (state = {}, action) => {
switch (action.type) {
// handle actions
default:
return state
}
}
const reducer2 = (state = {}, action) => {
switch (action.type) {
// handle actions
default:
return state
}
}
const reducer3 = (state) => {
return {
...state,
// use the root state for the reducerC value
reducerC: makeValue(state.reducer1, state.reducer2)
}
}
const rootReducer = reduceReducers(combineReducers({reducer1, reducer2}), reducer3)
This uses the library reduce-reducers to combine reducers in a different way.
I have 2 reducers that are combined in a Root Reducer, and used in a store.
First reducer 'AllTracksReducer" is supposed to return an object and the second 'FavoritesReducer' an array.
When I create a container component and a mapStateToProps method in connect, for some reason the returned state of the store is an object with 2 reducer objects which hold data, and not just an object containing correposding data, as expected.
function mapStateToProps(state) {
debugger:
console.dir(state)
//state shows as an object with 2 properties, AllTracksReducer and FavoritesReducer.
return {
data: state.AllTracksReducer.data,
isLoading: state.AllTracksReducer.isLoading
}
}
export default connect(mapStateToProps)(AllTracksContainer);
so, in mapStateToProps, to get to the right state property, i have to say
state.AllTracksReducer.data... But I was expecting the data to be available directly on the state object?
Yep, this is a common semi-mistake. It's because you're using likely using ES6 object literal shorthand syntax to create the object you pass to combineReducers, so the names of the imported variables are also being used to define the state slice names.
This issue is explained in the Redux docs, at Structuring Reducers - Using combineReducers.
Create some selectors that receive the whole state (or the reducer-specific state) and use it in your mapStateToProps function. Indeed the name you define when you combineReducers will be the topmost state keys, so your selectors should take that into account:
const getTracks = (state) => state.allTracks.data
const isLoading = state => state.allTracks.isLoading
This assumes you combine your reducers with allTracks as they key like here:
combineReducers({
allTracks: allTracksReducer
})
And then you can use those selectors in your mapper, like
const mapStateToProps = state => ({
isLoading: isLoading(state),
tracks: getTracks(state)
})
There's a delicate link between your combineReducers call and your selectors. If you change the state key name you'll have to update your selectors accordingly.
It helps me to think of action creators as "setters" and selectors as "getters", with the reducer function being simply the persistence part. You call your setters (dispatching action creators) when you want to modify your state, and use your selectors as shown to get the current state and pass it as props to your components.
Well, that's how it supposed to work. When you're using combineReducers, you're literally mapping the name of a reducer to the reducer function.
If it bothers you, I would suggest a little syntactical magic if you're using es2016 (though it seems you're not) like so:
function mapStateToProps(state) {
const { data, isLoading } = state.allTracksReducer;
return {
data: data,
isLoading: isLoading
}
}
export default connect(mapStateToProps)(AllTracksContainer);
Remember, state is the one source of truth that possesses all your reducers.