How can I avoid duplicating business logic in my Redux reducers? - redux

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.

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).

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

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.

How to work with Redux reducers and server API

What is a better way to create reducers with handleActions in redux-actions:
1. Create reducers for each CRUD operations (like add data, delete data) and combine it. How set initialState in this case?
2. Set actions in one reducer (fetchDeleteDataRequest, fetchDeleteDataSuccess, fetchAddDataRequest, fetchAddDataSuccess by example)?
You can have sperate reducers and or common reducers to add or delete data that is up to you. If you are considering separate actions for add and delete you will need to keep the data consistent. But having a common function to deal with the CRUD operations is ideal since this will reduce the amount of code that you have to use but you will need a way to distinguish them as well (maybe bypassing some variable ADD or DELETE). let's consider and list of items that you will be adding or deleting. This list can be an empty array in the beginning (initialState) and pass it as props to your component.
Actions
export const addDeleteItem = data => dispatch => {
// you can perform REST calls here
dispatch({
type: ADD_REMOVE_DATA,
payload: data
});
};
Reducers
let initialState = {
items:[]
}
export default (state = initialState, action) => {
switch (action.type) {
case ADD_REMOVE_DATA:
if(action.payload.event === 'ADD'){
return {...state,items:[...state.items,action.payload.item]}
}else if(action.payload.event === 'DELETE'){
return {...state,items:state.items.filter(item=>item.id !== action.payload.item.id)}
}else if (action.payload.event === 'UPDATE'){
//handle your update code
}
}
}
This is just an example you can follow something like this to avoid code duplication.

Reusability of reducers at different levels of app state structure

For example we have reducer photos, which handles array of photos via actions ADD_PHOTO and REMOVE_PHOTO. And if we have arrays users and posts, they both have field for array of photos.
So, in order to avoid code duplicates I'm going to do the following:
Create reducer user = combineReducers(..., photos, ...)
Create actionCreator updateUser
const updateUser = (id, subAction) => ({
type: UPDATE_USER,
payload: {
id,
subAction
}
})
Create reducer users (Here I'm using Immutable.js)
function users(state = List(), action) {
switch (action.type) {
//...
case UPDATE_USER:
const { id, subAction } = action.payload
const index = state.findIndex(user => user.id == id)
return state.updateIn(
[index, 'photos'],
state => photos(state, subAction)
)
break
//...
default:
return state
}
}
And then I'm going to use all of it like this:
dispatch(updateUser(id, addPhoto(url)))
Is this a correct solution of my problem?
Why not simply dispatch both in the place where this is initiated by the user?
dispatch(updateUser(id));
dispatch(addPhoto(url));
I haven't come across this pattern you're applying before. It seems a bit unusual that one reducer is responsible for reducing state of another. Creates a dependency between them that doesn't feel very pure. I'm not even sure one reducer should be able to/can see the state of another.
So no idea about "correct", but I'd say it's not ideal to do it your way. I'd try dispatching both sequentially or maybe in a sort of meta-action that takes care of nested updates and dispatches actions to multiple reducers.

Resources