Redux reducer design for large state within a single module - redux

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

Related

Redux state is being overwritten by other state change

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)

Redux reducers, object key value

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
}
};

Is adding conditions to a reducer is an anti pattern?

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.

redux-promise with Axios, and how do deal with errors?

So, I see on an error, redux-promise hands me back error: true, along with the payload, but that is once it hits the reducer... to me, decoupling the request AND error condition is a bit odd, and seems inappropriate. What is an effective way to also deal with error condition when using axios w/ reduc-promise (middleware).. here is the gist of what i have..
in action/
const request = axios(SOME_URL);
return {
type: GET_ME_STUFF,
payload: request
}
in reducer/
const startState = {
whatever: [],
error: false
}
case GET_ME_STUFF:
return {...state, startState, {stuff:action.payload.data, error: action.error? true : false}}
etc... then I can deal with the error.. so, my api call is now split into two seperate areas and that seems wrong.... there must be something I am missing here. I would think in the /actions I can pass in a callback that handles a new action etc.. or something, but not split it.
I've had to go through a similar situation. The challenge is that you likely won't be able to evaluate the results of the promise until it is at the reducer. You could handle your exceptions there but it's not the best pattern. From what I've read reducers are meant only to return appropriate pieces of state based on action.type and do nothing else.
So, enter an additional middleware, redux-thunk. Instead of returning an object, it returns a function, and it can coexist with promise.
It's explained quite well at http://danmaz74.me/2015/08/19/from-flux-to-redux-async-actions-the-easy-way/ [archived here]. Essentially, you can evaluate the promise here and dispatch through the other action creators before the promise result hits the reducers.
In your actions file, add additional action creators that would handle the success and error (and any other) states.
function getStuffSuccess(response) {
return {
type: GET_ME_STUFF_SUCCESS,
payload: response
}
}
function getStuffError(err) {
return {
type: GET_ME_STUFF_ERROR,
payload: err
}
}
export function getStuff() {
return function(dispatch) {
axios.get(SOME_URL)
.then((response) => {
dispatch(getStuffSuccess(response))
})
.catch((err) => {
dispatch(getStuffError(err))
})
}
}
return null
This is roughly to how you might translate your pseudocode to what is explained at the link. This handles evaluating the promise directly in your action creator and firing off the appropriate actions and payloads to your reducers which follows the convention of action -> reducer -> state -> component update cycle. I'm still pretty new to React/Redux myself but I hope this helps.
The accepted answer doesn't make use of redux-promise. Since the question is actually about handling errors using redux-promise I provide another answer.
In the reducer you should inspect the existence of the error attribute on the action object:
// This is the reducer
export default function(previousState = null, action) {
if (action.error) {
action.type = 'HANDLE_XHR_ERROR'; // change the type
}
switch(action.type) {
...
And change the type of the action, triggering a state change for an error handling component that you have set up for this.
You can read a bit more about it here on github.
It looks like you can catch the error where you make the dispatch, then make an separate error dispatch if it happens. It's a bit of a hack but it works.
store.dispatch (function (dispatch) {
dispatch ({
type:'FOO',
payload:axios.get(url)
})
.catch (function(err) {
dispatch ({
type:"FOO" + "_REJECTED",
payload:err
});
});
});
and in the reducer
const reducer = (state=initialState, action) => {
switch (action.type) {
case "FOO_PENDING": {
return {...state, fetching: true};
}
case "FOO_REJECTED": {
return {...state, fetching: false, error: action.payload};
}
case "FOO_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
data: action.payload,
};
}
}
return state;
};
Still using redux-promises you can do something like this which I think is an elegant way to deal with this problem.
First, set a property in the redux state that will hold any ajax errors that may occurred.
ajaxError: {},
Second, setup a reducer to handle ajax errors:
export default function ajaxErrorsReducer(state = initialState.ajaxError, action) {
if (action.error) {
const { response } = action.payload;
return {
status: response.status,
statusText: response.statusText,
message: response.data.message,
stack: response.data.stack,
};
}
return state;
}
Finally, create a very simple react component that will render errors if there are any (I am using the react-s-alert library to show nice alerts):
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Alert from 'react-s-alert';
class AjaxErrorsHandler extends Component {
constructor(props, context) {
super(props, context);
this.STATUS_GATE_WAY_TIMEOUT = 504;
this.STATUS_SERVICE_UNAVAILABLE = 503;
}
componentWillReceiveProps(nextProps) {
if (this.props.ajaxError !== nextProps.ajaxError) {
this.showErrors(nextProps.ajaxError);
}
}
showErrors(ajaxError) {
if (!ajaxError.status) {
return;
}
Alert.error(this.getErrorComponent(ajaxError), {
position: 'top-right',
effect: 'jelly',
timeout: 'none',
});
}
getErrorComponent(ajaxError) {
let customMessage;
if (
ajaxError.status === this.STATUS_GATE_WAY_TIMEOUT ||
ajaxError.status === this.STATUS_SERVICE_UNAVAILABLE
) {
customMessage = 'The server is unavailable. It will be restored very shortly';
}
return (
<div>
<h3>{ajaxError.statusText}</h3>
<h5>{customMessage ? customMessage : ajaxError.message}</h5>
</div>
);
}
render() {
return (
<div />
);
}
}
AjaxErrorsHandler.defaultProps = {
ajaxError: {},
};
AjaxErrorsHandler.propTypes = {
ajaxError: PropTypes.object.isRequired,
};
function mapStateToProps(reduxState) {
return {
ajaxError: reduxState.ajaxError,
};
}
export default connect(mapStateToProps, null)(AjaxErrorsHandler);
You can include this component in your App component.
This might not be the best approach but it works for me. I pass the 'this' of my component as var context. Then when i get response back i just execute the methods defined in my components context. In my component i have successHdl and errorHdl. From there i can trigger more redux actions as normal. I checked all the previous answers and seem too daunting for such a trivial task.
export function updateJob(payload, context){
const request = axios.put(UPDATE_SOMETHING, payload).then(function (response) {
context.successHdl(response);
})
.catch(function (error) {
context.errorHdl(error);
});;
return {
type: UPDATE_SOMETHING,
payload: payload,
}
}
Don't use redux-promise. It overcomplicates something that's actually super simple to do yourself.
Instead read the redux docs: http://redux.js.org/docs/advanced/AsyncActions.html
It'll give you a much better understanding of how to handle this kind of interactions and you'll learn how to write something (better than) redux-promise yourself.

Can combineReducers work with extra actions?

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 .

Resources