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.