I have a cart items in my store and i want to check if the user added the same item with the same color and size.
If so i just want to increase the quantity, if not just add it but it creates kinda akward reducer and i wanted to know if this considered as an anti pattern and if so, what can we do to simplify it
export default (state = initialState, action) => {
switch (action.type) {
case 'ADD_TO_CART':
// Check to see if we already have the same product
// with the same color and size in our cart
const productIndex = state.products
.findIndex(product => product._id === action.product._id
&& product.size === action.product.size
&& product.color === action.product.color);
// if we have it already increase the quantity
if (productIndex !== -1) {
const updatedProduct = {
...state.products[productIndex],
quantity: state.products[productIndex].quantity + action.product.quantity,
};
return {
...state,
products: [
...state.products.slice(0, productIndex),
updatedProduct,
...state.products.slice(productIndex + 1),
],
};
}
// if not just save it
return {
...state,
products: [...state.products, action.product],
};
default:
return state;
}
};
No, it's absolutely not an anti-pattern. Reducers can, and should, do a lot more than just return {...state, ...action.payload}. Now, what logic you put in a reducer is up to you, as well as what abstractions you choose to use in the process.
If you're concerned about writing nested immutable update logic, then I'd suggest looking at the immer immutable update library. I'd also suggest trying out our new redux-starter-kit package, which uses immer internally.
Related
I have come across two kinds of reducer design for handling a large state within a single module.
The first approach is to have all the variables inside a single large state and have one reducer function.
const initialState = {
results: [],
pagination: {},
filters: [],
appliedFilters = [],
}
const reducer = (st = { ...initialState }, action) => {
const state = st;
switch (action.type) {
case 'SEARCH':{
return {
...state,
results: action.results,
pagination: action.pagination,
filters: action.filters,
appliedFilters: action.appliedFilters
},
case 'APPLY_FILTER':{
return {
...state,
results: action.results,
pagination: action.pagination,
filters: action.filters,
appliedFilters: action.appliedFilters
},
case 'PAGINATE':{
return {
...state,
results: action.results,
pagination: action.pagination,
}
}
The second approach is to have multiple reducers for the sub items in the data.
export function applications(state = [], { type, results}) {
switch (type) {
case SEARCH:
return results;
case INIT_RESULTS:
return [];
default:
return state;
}
}
export function pagination(state = null, { type, paginationData }) {
switch (type) {
case SEARCH:
return paginationData;
default:
return state;
}
}
export function filters(state = [], { type, filterData }) {
switch (type) {
case SEARCH:
return filterData;
case UPDATE_FILTERS:
return filterData;
default:
return state;
}
}
I think both have their own pros and cons. Considering scalability and modularization which one is a better pick?
Generally, both of these are very far off our official recommendations.
you should have a "slice" reducer for each sub-state (that rules out your first option
you should not treat reducers as "setting a value", but move the whole "calculating how to get the value" into the reducer and handle your action as just "describing an event that happened"
you should be using the official Redux Toolkit which we are recommending & teaching as the default way of writing Redux sinde 2019. Seriously, look at it. It is about 1/4 of the code. No more switch..case reducers or ACTION_TYPES.
Please give the Redux Style Guide a read and to learn modern Redux with Redux Toolkit, please follow the official Redux Tutorial
I am new to redux and I was testing it by creating a theme toggler and a counter.
the problem is everytime i click to increment the counter that works fine but when i try to toggle the dark theme the count resets to 0 and vice versa. I am using combineReducers which holds the themeReducer and counterReducer.
Why is the counter getting reset on state change? what am i doing wrong?
here is some code:
Actions:
export function increment() {
return {
type: 'INCREMENT',
};
}
export function decrement() {
return {
type: 'DECREMENT',
};
}
//different file
export function darkTheme() {
return {
type: 'DARK',
};
}
Reducers:
export default function counterReducer(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return 0;
}
}
//different file
export default function themeReducer(state = false, action) {
switch (action.type) {
case 'DARK':
return !state;
default:
return false;
}
}
im calling it like this :
const dispatch = useDispatch();
<button onClick={() => dispatch(increment())}> +</button>
You have to always return state in the default case, in both reducers: every reducer will always be called with every action, so they have to do no modification to the existing state in the default case.
All that said, I want to make you aware that you are learning "vanilla redux" here, which is not the recommended style of redux for production use any more. So while that is great to get an understanding of the underlying concepts, you should not really write code like this in the end. Please follow the official redux tutorials to get an understanding on how to write modern redux. (The fundamentals tutorial will get you started with the style you are using right now and then move into more modern territory, the essentials tutorial will start directly with modern redux - both ways are valid for learning redux)
I have hashMap in my redux store, I want change isChecked value for children id: 2. Is it good to make it on state like this (operating on state)?
My hashMap
const childrens = {
1: { name: "Test", isChecked: false },
2: { name: "test2", isChecked: false }
};
Here is my reducer
export const childrensReducer = (state = childrens, action) => {
switch (action.type) {
case "SELECT_CHILDREN":
const id = 2;
state[id].isChecked = !state[id].isChecked;
return { ...state };
}
};
The problem is that you are mutating the state in the reducer with this line:
state[id].isChecked = !state[id].isChecked;
Why immutability is required by redux can be found in official docs:
https://redux.js.org/faq/immutable-data
One way to do is: ( I expect you send id through action.id )
case "SELECT_CHILDREN":
return {
...state,
[action.id]: {
...state[action.id],
isChecked: !state[action.id].isChecked
}
};
These kind of state operations are easier when an array is used for state.
It's not a good practice to mutate the state like you did.
There are different approaches of changing the state. Take a look at the below link to get some more information and examples.
https://www.freecodecamp.org/news/handling-state-in-react-four-immutable-approaches-to-consider-d1f5c00249d5/
Its not a good practise to mutate state, since react depends on immutability for a lot of its features.
Consider for example lifecycle methods or rerender after comparing state/props(PureComponents)
The problem with mutating state is that when these values are passed as props to children and you try to take some decision on them based on whether the state has updated, the previous props and the current props both will hold the same value and hence the comparisons may fail leading to buggy application
The correct way to update state is
case "SELECT_CHILDREN":
const id = 2;
return {
...state,
[id]: {
...state[id],
isChecked: !state[id].isChecked
}
};
I have a (ngrx) store for an array of Speaker object and for the SelectedSpeaker. The reducer looks like:
export const speakers = (state: any = [], { type, payload }) => {
switch (type) {
case SpeakerActions.TOGGLEFAVORITE:
return state.map(speaker => {
return speaker.id === payload.id ? _.assign({}, speaker, {isFavorite: !speaker.isFavorite}) : speaker;
});
}
}
I left out the unimportant code. The reducer for currentSpeaker looks like:
export const selectedSpeaker = (state: any = [], { type, payload }) => {
switch (type) {
case SelectedSpeakerActions.SELECT:
return payload;
}
}
Now my question, if I dispatch a SpeakerActions.TOGGLEFAVORITE for a speaker and this happens to be the SelectedSpeaker, how do I update the SelectedSpeaker in this case? Note this all works as part of an Angular2 project, for what that worth.
Generally, Redux state should be fully normalized - you shouldn't have some state in two places, since it creates exactly the problem you are seeing.
Probably the best solution in your case is for selectedSpeaker just to contain the id of the selected speaker, not the speaker itself. e.g. something like
export const selectedSpeaker = (state: any = [], { type, payload }) => {
switch (type) {
case SelectedSpeakerActions.SELECT:
return payload.id;
}
}
Obviously, you'll need to lookup the selected speaker where you use it, using the ID. You might also find it easier to have an object (or Map) from id=>speaker in your speaker store, rather than a plain array.
What I want is the root reducer combine other reducers, and listen to extra actions. I've find the docs, but I can not get any information.
Here is some pseudo code.
const root1 = combineReducers({
reducer1,
reducer2,
reducer3,
reducer4
});
function root2(state = initState, action) {
switch (action.type) {
case LOAD_DATA:
return _.assign({}, initState, action.data);
default:
return state;
}
}
merge(root1, root2);
The only way I figure out is to drop combineReducers:
function root(state = initState, action) {
switch (action.type) {
case LOAD_DATA:
return _.assign({}, initState, action.data);
case ...: return ...;
case ...: return ...;
case ...: return ...;
default: return state;
}
}
Is there another way to implement this?
Yes, you can use combineReducers() with multiple reducers while also having an action that rebuilds your entire application state. Admittedly, that is a bit of a strange design decision and does not scale very well with more complex apps, but you obviously have a use-case. If you want to do something like that you have two choices.
Option 1: Divide up action
It is totally valid to listen for the same action type within multiple reducer functions. This is the most straightforward approach, although it involves more repetition. You would just break out each piece of state returned by your action into the individual reducer functions it applies to.
For instance, if this was your entire application state
{
foo: {},
bar: {}
}
And your action type that rebuilt the entire application state was LOAD_DATA, you could do this
function foo (state = {}, action) {
switch (action.type) {
case 'LOAD_DATA':
return {...state, action.result.foo}
}
}
function bar (state = {}, action) {
switch (action.type) {
case 'LOAD_DATA':
return {...state, action.result.bar}
}
}
const root = combineReducers({
foo,
bar
});
With that, both foo and bar in your state would always get rebuilt with the corresponding data coming from the same action.
Option 2: Build Custom combineReducers()
There is nothing stopping you from building your own version of combineReducers(). If you watch this video on building a combineReducers() function from scratch, you'll see that the logic in place is not that complicated. You would just have to listen for the specific action type and return the entire state from that action if it matched. Here's a version of that I built by looking at the current source for combineReducers() and then working the 2 util functions into that function
function combineReducers(reducers) {
var fn = (val) => typeof val === 'function';
var finalReducers = Object.keys(reducers).reduce((result, key) => {
if (fn(reducers[key])) {
result[key] = reducers[key]
}
return result
}, {});
return function combination(state = {}, action) {
if (action.type === 'LOAD_DATA') {
return completeStateReducer(action)
} else {
var hasChanged = false
var fn = (reducer, key) => {
var previousStateForKey = state[key]
var nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
var errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
return nextStateForKey
}
var finalState = Object.keys(finalReducers).reduce((result, key) => {
result[key] = fn(finalReducers[key], key)
return result
}, {})
return hasChanged ? finalState : state
}
}
}
function completeStateReducer(action) {
return action.result;
}
Outside of merging those util functions back in, the only thing I really added was the bit about listening for the LOAD_DATA action type and then calling completeStateReducer() when that happens instead of combining the other reducer functions. Of course, this assumes that your LOAD_DATA action actually returns your entire state, but even if it doesn't, this should point you in the right direction of building out your own solution.
First, combineReducers is merely a utility function that simplifies the common use case of "this reducer function should handle updates to this subset of data". It's not required.
Second, that looks like pretty much the exact use case for https://github.com/acdlite/reduce-reducers. There's an example here: https://github.com/reactjs/redux/issues/749#issuecomment-164327121
export default reduceReducers(
combineReducers({
router: routerReducer,
customers,
stats,
dates,
filters,
ui
}),
// cross-cutting concerns because here `state` is the whole state tree
(state, action) => {
switch (action.type) {
case 'SOME_ACTION':
const customers = state.customers;
const filters = state.filters;
// ... do stuff
}
}
);
Also, I give an example of using reduceReducers in the "Structuring Reducers" section of the Redux docs: http://redux.js.org/docs/recipes/reducers/BeyondCombineReducers.html .