Redux has been something that I've seen on job descriptions for a long time. Some developers tell me it's the only acceptable option, other tell me it's not an option at all. Is it a Marmite state management tool?
Well we are going to look into what Redux is and what it's advantages and disadvantages are.
Why Redux?
Redux is a JavaScript library for managing and centralising an application's state. It was created in 2015 by Dan Abramov and Andrew Clark. Abramov himself worked at Facebook, so when discovered this library I was tempted to it by the fact it was coming from Facebook employees themselves.
There are alternatives like MobX and Recoil that provide a similar state management. But Redux get a fair amount of coverage. So when you are looking for examples of how to implement and use Redux, you're going to be able to find a lot of examples on StackOverflow, Medium and other article sites.
Redux setup
With Redux, there is quite a bit of code setup required. First is your store. This is where you'll be putting all the state.
//app.tsx
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import rootReducer from './reducers';
import App from './components/App';
const store = createStore(rootReducer);
render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
In the code example above the store is created and supplied to a <Provider />
element from react-redux
that wraps round the whole <App />
.
Next you're going to need some middleware to deal with handling store access asynchronously. Redux
suggests redux-thunk
.
To implement this middleware is going to be a lot of code for something you will rarely change. You're also going to be
refining your rootReducer
here which handles all the calls to alter the store state.
//configureStore.ts
import {applyMiddleware, compose, createStore} from 'redux';
import thunkMiddleware from 'redux-thunk';
import monitorReducersEnhancer from './enhancers/monitorReducers';
import loggerMiddleware from './middleware/logger';
import rootReducer from './reducers';
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware];
const middlewareEnhancer = applyMiddleware(...middlewares);
const enhancers = [middlewareEnhancer, monitorReducersEnhancer];
const composedEnhancers = compose(...enhancers);
const store = createStore(rootReducer, preloadedState, composedEnhancers);
return store;
}
Action Creators
To make changes to your state, you're going to need Actions (or Action Creators here). When you dispatch a request the state management will be looking for your type to work out what you want to do.
{
type: 'ADD_PRODUCT', product
:
{...
}
}
{
type: 'REMOVE_PRODUCT', productId
:
1
}
Reducers
The Actions will then be inspected by the Reducers. You have one rootReducer for your whole state, where you can then bring in your specific reducers. You could put it all in one but the code base is getting big enough already to be confusing.
// rootReducer.ts
export const rootReducer: Reducer<IStoreState, Actions> = combineReducers({
cartState: reduceCartState,
});
And then you have your specific reducers. Here it's my standard cart example.
// cartReducer.ts
import {Reducer} from "react";
const initialState: CartState = {
items: []
};
export const reduceCartState: Reducer<CartState, CartActions>
createReducer({}, {
[ActionTypes.ADD_PRODUCT]: (state, action) => {
const cart = addProduct(action);
return {...state, items: cart};
},
[ActionTypes.REMOVE_PRODUCT]: (state, action) => {
const cart = removeItem(action)
return {...state, items: cart};
}
})
So in this cartReducer it's looking for the two action types and then acting on what is passed through. Functions defined to take the current state and add or remove a product. Those functions are omitted here but easy to implement.
Conclusion
The code above it a simple example of setting up a state with a simple add remove. It's a lot of code to get started with. It's not particularly readable because you're going to be jumping from actions to reducers to state files. If you try out the devtools that come with Redux it can show you the state, the history of the state, and what actions are being called, but it is quite a pain when you want to change things, and with TypeScript you'd hope that there would be some helpful errors to point out your errors, but not with Redux (at the time of writing this).
Other newer libraries like ReactQuery can manage these types of state management with simple calls. Viewing the state is as easy and Redux using the devtools supplied, and mutating the state is much easier. It also has type safety (like Redux) and supports Vue, Solid and Svelte.
But on the positives of Redux, it will hold you to doing things the way you've set them up. You're not going to find yourself with state in the wrong place. You're not going to be fixing bugs for missing data that you end up wasting hours trying to figure out. If you set up your test library using the full mocked state ,you're also going to find a good trace of problems when the runner is doing its job.
If a project had Redux on it, I'd be happy to use it. But if a project needed state management I'd recommend an alternative.