Redux: is it an anti-pattern to put much logic and share date between slice reducers in thunks? - redux

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.

Related

Redux Toolkit says this snippet(19 lines) is shorter code (vs. original 12) confusion

I am reading this page getting into react-redux https://redux.js.org/introduction/getting-started
I am very confused looking at the Basic Example which has 12 lines of code(excluding usage, imports, and comments)
Then I read this line on the "Redux Toolkit Example" which below the code states "Redux Toolkit allows us to write shorter logic that's easier to read, while still following the same Redux behavior and data flow." However, this example is 19 lines of code(excluding usage, imports, and comments)
The usage in both examples is 3 lines of code. Could someone explain this to me?
Perhaps when it scales, the redux toolkit example does save more code? Honestly, I find the Basic Example MUCH easier to read and maintain. NOTE: I am a complete newb which leads me to believe the basic example may be better as we ramp up and hire developers. This allows them to ramp up more quickly(but I am only a single data point amongst newbs)
thanks,
Dean
You're right about the lines of code in the example. Perhaps that simple counter example doesn't do justice to how much code Redux Toolkit can save because they aren't adding all the "bells and whistles" in their non-toolkit version.
This section is called "getting started with Redux" rather than "migrating to Redux Toolkit" so I suspect they don't want to overwhelm the user by introducing best practices like action creator functions which aren't strictly necessary. But you're not seeing the "write less code" benefit because most of the code that you can remove with the Toolkit is coming from things that weren't in the example in first place.
Action Creators
One of the main benefits of the createSlice function is that it automatically creates the action name constants and action creator functions to go along with each case in the reducer.
This example is just dispatching raw actions directly with string names store.dispatch({ type: 'counter/incremented' }). Most devs would not do this because of how fragile and inflexible it is.
An alternate version of the non-toolkit example, what you would see in most code, looks more like this:
// action name constants
const INCREMENTED = 'counter/incremented';
const DECREMENTED = 'counter/decremented';
// action creator functions
// usually most take some arguments
const incremented = () => ({
type: INCREMENTED,
})
const decremented = () => ({
type: DECREMENTED,
})
// reducer function
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case INCREMENTED:
return { value: state.value + 1 }
case DECREMENTED:
return { value: state.value - 1 }
default:
return state
}
}
If you want to include typescript types it gets even worse.
Immutability
The reducer itself could get really lengthy if you are trying to do immutable updates on deeply nested data.
Here's an example copied from those docs on how to safely update the property state.first.second[someId].fourth
Without Toolkit
function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
With Toolkit:
const reducer = createReducer(initialState, {
UPDATE_ITEM: (state, action) => {
state.first.second[action.someId].fourth = action.someValue
}
})
configureStore
The Toolkit configureStore actually does save a step vs the Redux createStore function when you are combining more than one reducer. But again this example fails to show it. Instead the Toolkit version is longer because we set a reducer property rather than just passing the reducer.
A typical Redux app uses the combineReducers utility to combine multiple reducers as properties on an object:
import {createStore, combineReducers} from "redux";
const rootReducer = combineReducers({
counter: counterReducer,
other: otherReducer
});
const vanillaStore = createStore(rootReducer);
With the Toolkit you can just pass your reducers map directly without calling combineReducers.
import {configureStore} from "#reduxjs/toolkit";
const toolkitStore = configureStore({
reducer: {
counter: counterReducer,
other: otherReducer
}
});
Which is roughly the same amount of code. But it also includes some default middleware which would be extra lines in the non-toolkit example.

Slice's State self reference inside reducer

When updating my state inside a slice's reducer with the redux's toolkit, I ran into the problem of the circular reference, for example:
const aSlice = createSlice({
...
extraReducers: builder => {
...,
builder.addCase(addToState.fulfilled, (state, action) => {
state.data = {
...state.data,
...action.payload.data
};
});
...,
}
});
Thus resulting in ...state.data returning a proxy reference instead of the value which is one of the pitfalls mention, Redux Toolkit doc as well as in Immer.js pitfalls section.
I can think of some ways to hack this into working but, I was wondering if they were any best practice for this matter or any elegant way of making this work?
When using Immer and proxies, a useful pattern for "assign all" is to actually use Object.assign().
Typically, use of Object.assign() with Redux involves passing an empty object as the first parameter to make it an immutable update, like Object.assign({}, oldItem, newItem).
However, with Immer, you can do a truly mutating update to assign all incoming properties to the existing object. In this case, Object.assign(state.data, action.payload.data).

Can I pass always the full state to reducers?

Is there any inconvenient at all if I design my reducers to, instead of reading only the partial state, had access to the full state tree?
So instead of writing this:
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
I destructure state inside doSomethingWithA, c or processB reducers, separately:
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state, action), // calc next state based on a
b: processB(state, action), // calc next state based on b
c: c(state, action) // calc next state based on a, b and c
}
}
Would I'd be using more RAM? Is there any performance inconvenient? I understand that in javascript, a reference is always passed as parameter, that's why we should return a new object if we want to update the state or use Immutable.JS to enforce immutability, so... again, would it be of any inconvenient at all?
No, there's nothing wrong with that. Part of the reason for writing update logic as individual functions instead of separate Flux "stores" is that it gives you explicit control over chains of dependencies. If the logic for updating state.b depends on having state.a updated first, you can do that.
You may want to read through the Structuring Reducers section in the Redux docs, particularly the Beyond combineReducers topic. It discusses other various reducer structures besides the typical combineReducers approach. I also give some examples of this kind of structure in my blog post Practical Redux, Part 7: Form Change Handling, Data Editing, and Feature Reducers.

React Redux - state returned in mapStateToProps has reducer names as properties?

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.

"Thread safety" in Redux?

Let's pretend I have a long-running function working on computing my new state.
Meanwhile another action comes in and changes the state while the first one did not finish and is working on stuff.
If I am imagining things correctly there is no actions queue and the state might be resolved in some unpredictable manner.
Should I be worried about this at all?
I don't mean real threads, just a concept for the lack of better wording. Actions are asynchronous and state keys are being accessed by reference.
I was concerned about the same thing so I just did some digging. It looks like two threads concurrently calling dispatch() (if it were possible) could raise an exception. But it shouldn't be possible and that error message points to a particular, different cause. The "actions queue" is in the browser's own event loop. That event loop runs async/interaction callbacks (from which we call dispatch()) one-at-a-time.
That's the responsibility of your own action creators and your own reducers, and heavily related to how you structure your actions and reducers conceptually. The Redux FAQ question on structuring "business logic" is very relevant here:Redux FAQ
Thunk action creators have access to getState, so it's very common to have a thunk check the current state and only dispatch under certain conditions, such as this example:
// An example of conditional dispatching based on state
const MAX_TODOS = 5;
function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState();
if(state.todos.length < MAX_TODOS) {
dispatch({type : "ADD_TODO", text : todoText});
}
}
}
Your reducer can also have sanity checks as well:
function todosReducer(state, action) {
switch(action.type) {
case "ADD_TODO": {
if(state.todos.length >= state.maxTodos) {
return state;
}
return {
...state,
todos : state.todos.concat(action.newTodo)
}
}
default : return state;
}
}
Personally, I don't like to have my reducers just blindly merge in whatever data's in the action, unless it's very small (like, say, the name of the currently selected tab or something). I prefer to have a reasonable amount of logic in my action creator to set up the action, a minimal-ish amount of data included in the action itself, and a sufficiently smart reducer to do the work based on that action.

Resources