Managing application state becomes challenging as a React application grows. Data that starts in one component often needs to be shared across multiple screens, updated by different user interactions, and kept consistent when asynchronous operations are involved.
Redux Toolkit (RTK) simplifies Redux by providing utilities for creating stores, defining reducers, and writing predictable state updates with less boilerplate. It includes tools such as configureStore, createSlice, and createAsyncThunk, making it easier to build maintainable state management solutions.
In this guide, we will build a runnable JavaScript example of a shopping cart using Redux Toolkit. You will learn how to structure state, create actions, handle updates, calculate derived values, and test the Redux workflow without needing a browser or a React application.
Building a Shopping Cart with Redux Toolkit
A Redux Toolkit application starts with a store, which holds the shared application state. A slice defines a particular part of that state, its initial values, and the reducers responsible for updating it. Redux Toolkit uses Immer internally, allowing reducer logic to use mutation-like syntax while producing immutable state updates.
For a shopping cart, the state might contain an array of products with their IDs, names, prices, and quantities. We can define reducers for adding products, removing products, changing quantities, and clearing the cart. Each reducer becomes an action creator automatically, so components or other JavaScript modules can dispatch these actions instead of manually constructing action objects.
The following example uses Node.js with ES modules and the Redux Toolkit package. It demonstrates the complete store lifecycle through console logs, including initial state, dispatched actions, updated quantities, subtotal calculations, and cart cleanup. The example deliberately keeps business logic inside the slice and uses selectors to calculate derived information, making the design easier to extend and test.
To run it, create a project with npm init -y, install Redux Toolkit using npm install @reduxjs/toolkit, and save the code as index.js. Add "type": "module" to package.json, then execute node index.js. The example runs independently of React, although a React application can later connect the same store using react-redux and the Provider component.
import { configureStore, createSlice } from '@reduxjs/toolkit';
// Define the initial state for the shopping cart.
const initialState = {
items: [],
currency: 'USD',
};
// Create a slice containing state and related reducer logic.
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem(state, action) {
const product = action.payload;
const existingItem = state.items.find(
(item) => item.id === product.id
);
if (existingItem) {
// Increase the quantity when the product already exists.
existingItem.quantity += 1;
} else {
// Add a new product with an initial quantity of one.
state.items.push({ ...product, quantity: 1 });
}
},
removeItem(state, action) {
// Remove the product matching the supplied identifier.
state.items = state.items.filter(
(item) => item.id !== action.payload
);
},
updateQuantity(state, action) {
const { id, quantity } = action.payload;
const item = state.items.find((product) => product.id === id);
// Reject invalid quantities instead of storing inconsistent data.
if (!Number.isInteger(quantity) || quantity < 1) {
return;
}
if (item) {
item.quantity = quantity;
}
},
clearCart(state) {
// Empty the cart while preserving other slice-level settings.
state.items = [];
},
},
});
// Configure the Redux store with the cart reducer.
const store = configureStore({
reducer: {
cart: cartSlice.reducer,
},
});
// Export generated actions for use by application components.
const { addItem, removeItem, updateQuantity, clearCart } = cartSlice.actions;
// Selectors keep state-reading logic reusable and consistent.
const selectCartItems = (state) => state.cart.items;
const selectItemCount = (state) =>
selectCartItems(state).reduce((total, item) => total + item.quantity, 0);
const selectSubtotal = (state) =>
selectCartItems(state).reduce(
(total, item) => total + item.price * item.quantity,
0
);
// Print a readable snapshot after each operation.
function logCart(label) {
const state = store.getState();
console.log(`\n--- ${label} ---`);
console.log('Items:', JSON.stringify(selectCartItems(state), null, 2));
console.log('Total units:', selectItemCount(state));
console.log('Subtotal:', selectSubtotal(state).toFixed(2), state.cart.currency);
}
// Subscribe to state changes to observe Redux updates.
const unsubscribe = store.subscribe(() => {
console.log('Store updated successfully.');
});
console.log('Step 1: Inspect the initial application state.');
logCart('Initial cart');
console.log('\nStep 2: Add a laptop to the cart.');
store.dispatch(addItem({ id: 'laptop', name: 'Laptop', price: 899.99 }));
logCart('After adding laptop');
console.log('\nStep 3: Add headphones to the cart.');
store.dispatch(addItem({ id: 'headphones', name: 'Headphones', price: 79.99 }));
logCart('After adding headphones');
console.log('\nStep 4: Add the laptop again to increase its quantity.');
store.dispatch(addItem({ id: 'laptop', name: 'Laptop', price: 899.99 }));
logCart('After adding another laptop');
console.log('\nStep 5: Change the headphones quantity to three.');
store.dispatch(updateQuantity({ id: 'headphones', quantity: 3 }));
logCart('After quantity update');
console.log('\nStep 6: Attempt an invalid quantity update.');
store.dispatch(updateQuantity({ id: 'headphones', quantity: 0 }));
logCart('After rejected quantity update');
console.log('\nStep 7: Remove the laptop from the cart.');
store.dispatch(removeItem('laptop'));
logCart('After removing laptop');
console.log('\nStep 8: Clear the remaining cart items.');
store.dispatch(clearCart());
logCart('After clearing cart');
// Stop listening when this demonstration is complete.
unsubscribe();
console.log('\nDemo complete. Final state:', JSON.stringify(store.getState()));
Conclusion
Redux Toolkit provides a structured approach to managing shared application state without requiring developers to write repetitive Redux boilerplate. By combining configureStore, createSlice, generated action creators, and reusable selectors, you can keep state transitions predictable and business logic organized.
As your application grows, consider separating slices by feature, keeping derived values in selectors, and handling API requests with createAsyncThunk or RTK Query. In React applications, use react-redux hooks such as useSelector and useDispatch to connect components to the store while avoiding unnecessary prop drilling.
The most important principle is to introduce global state only when it solves a real coordination problem. Local component state remains appropriate for isolated UI interactions, while Redux Toolkit is valuable when multiple parts of an application need a consistent, shared source of truth. Which part of Redux Toolkit would you explore next: asynchronous API requests, middleware, or performance optimization?
Top comments (1)
Running the shopping cart example in plain Node without React is a nice way to show the store is independent of the UI. One suggestion for the subtotal part: using createSelector from RTK (Reselect) memoizes the derived value, so it only recomputes when the cart items change. For money, storing prices as integer cents also avoids floating point rounding surprises in the subtotal.