Recently I stumbled upon the codebase where every reducer looks like the following. I guess they are spreading initialState to get rid of some 'leftover' nested state when reducer initialises, but is it meaningful?
const initialState = {...}
const reducer = (state = {...initialState}, action) => {
...
}
If your reducer doesn't violate immutability principle, spreading an initialState does not make any sense, since reducer returns a new state (e.g. using aforementioned spread operator) on every action anyway.
Related
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.
Lets say I have an application that displays issues for a given project. A user can open an issue, meaning they can see it on the screen, or close the issue, meaning it disappears. When I close the project, I want to also hide the issue visible to the user.
How can I avoid duplicating the business logic for mutating the state within the reducer? I can think of three options, and am wondering which is best, or what alternatives are available.
Idea one: Just repeat the code. I copy the CLOSE_PROJECT code into any method that needs it, like CLOSE_ISSUE.
const reducer = (state, action) => {
switch (action.type) {
case OPEN_PROJECT:
state.project = action.payload.project
case CLOSE_PROJECT:
state.project = null
state.issue = null
case CLOSE_ISSUE:
state.issue = null
}
}
Idea two: Move re-used code into helper functions. Pass the state into the helper function, get back the new state. (Note: I am using immer.js, but just picture this as psuedo code that doesn't actually mutate the state)
const closeProject = (state, action) => {
state.project = null
state
}
const closeIssue = (state, action) => {
state.project = null
state
}
const reducer = (state, action) => {
switch (action.type) {
case OPEN_PROJECT:
state.project = action.payload.project
case CLOSE_PROJECT:
state = closeProject(state)
state = closeIssue(state)
case CLOSE_ISSUE:
state.issue = null
}
}
Idea three: Handle the logic outside of the reducer. Have helper functions that co-ordinate multiple dispatch calls.
const closeProject = (dispatch) {
dispatch(closeProjectAction())
dispatch(closeIssueAction())
}
const ReactThing = (dispatch) => {
const handleCloseProjectClick = () => {
closeProject(dispatcher)
}
return (
<div onClick={ e => handleCloseProjectClick() }>Close</div>
)
}
I think idea three is the best. But it feels strange to have all these business logic functions just kind of floating around outside of Redux. Are there better alternatives?
All three options are entirely valid. To some extent, it's a question of how you want to model your logic, and how you want to abstract common functionality.
There's a couple sections of the Redux docs that mostly address your questions:
Redux FAQ: Where should my "business logic" live?
Structuring Reducers: Reusing Reducer Logic
I also discussed several aspects related to this in my post The Tao of Redux, Part 2: Practice and Philosophy.
Also, as a side note: I strongly recommend using our Redux Starter Kit package, which uses Immer internally for simplified reducer setup.
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.
In this github redux example, a dispatch of the event ADD_TODO is used to add a task. During the debugging, I found out that adding a task causes both the reducers todos and visibilityFilter being called.
How can I call just the todos reducer and not visibilityFilter reducer when I add a task. Also the visibilityFilter reducer if I sent an event of type SET_VISIBILITY_FILTER.
The combineReducers utility intentionally calls all attached reducer functions for every action, and gives them a chance to respond. This is because the suggested Redux reducer structure is "reducer composition", where many mostly-independent reducer functions can be combined into one structure, and many reducer functions could potentially respond to a single action and update their own slice of state.
As mentioned in other answers, combineReducers calls every reducer whenever a dispatch is called. You can avoid the other values changing, by making the default case equal to the state parameter passed in, so essentially they are reassigned their current value.
For example:
const individualReducer = (state = "initialState", action) => {
switch(action.type)
{
case "ACTION_TYPE":
return action.payload;
default:
return state;
}
}
export default individualReducer;