Working with Redux Store
I’ve recently been building a React project to manage a summer vacation retreat with multiple cabins. Having never used Redux, and after having set up several of the key components using React local state, I decided there’s no time like the present to learn Redux and refactor.
Before tackling my project I found it useful to set up a Redux playground to get feel for the tool. I created a store and provided it to my other components. Next I set up a reducer file that allows me to create, organize and edit the functions that Redux uses to manipulate the store, the reducer (the workers). Finally I set up a file where I can write and export my actions, the functions that are dispatched to the appropriate reducer (the messengers).
Once set up, I opened up the Redux-Devtools extension within Chrome:
This is an essential tool for viewing the current contents of my Redux state (store). It’s wired to the React App by adding the following line to your createStore() command:
const store = createStore(
testReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
If you’re new to Redux, I recommend setting up a basic test-bed React-Redux project, adding a few test actions with corresponding reducers just to get a feel for what’s going on. Dispatch the action(s) with a variety of data structures, and see how they affect your store using Redux DevTools.
Here’s some sample data to play with, an array of strings and an array of bike objects:
const cars = ['Ford', 'BMW', 'Tesla'];
const bikes = [
{ brand: 'Trek', model: 'All Terrain' },
{ brand: 'Schwinn', model: 'GTX3 Hybrid' },
{ brand: 'Giant', model: 'Rock Climber' }
]
I wrote a four sample actions: addCar, addCars, addBike, addBikes that take a similar form to test the results of my reducers.
export const addCar = (item) => {
return {
type: “ADD_CAR”,
payload: item
}
}
Here is test reducer with cases for a few of our actions:
export default function testReducer( state= {cars: []} ,
action
) {
console.log(action);
switch (action.type) { case ‘ADD_CARS’:
return { cars: action.payload } // return Object.assign({}, {cars: action.payload}) case ‘ADD_BIKES’:
// return { …state, items.concat(action.payload)}
return Object.assign({}, { bikes: action.payload })
default:
return state;
}
}
Notice that reducers require two arguments: state, and an action. Every time a React app fires up, Redux sends an initializing action to its reducers just to make sure they are set up correctly. The state argument in the reducer allows you to set a default state. If you leave the reducer’s state argument as simply ‘state’, Redux will function but your store will be undefined until you call a reducer. I discovered this is a real issue as you begin to seed components with data derived from your store. Initial renderings of these components will not find anything in the store and throw an error. You have two options:
- You can place conditionals around the renderings to check for the existence of the data before trying to render. Or
- You can use the Reducer’s default state to control what your components see initially. I found that this solution was generally the better one. If you are expecting to store an array of objects (like our array of cars), set a car object with an empty array as the default. If, in one of your components, you need to map through the car array, the empty container will not cause the app to crash, but rather act as a place holder until you’ve added data.
Once again, I highly recommend playing around with adjusting this default state and watching how it affects the store in Redux-Dev-Tools. For me, seeing is believing.
A quality of the Redux store that is critical to understand is that the store is immutable.
The store’s immutability requires that your reducers do not manipulate the store directly but rather return a new copy to replace the existing. This is on purpose as it allows Redux to save a history of its state and because it’s faster. In a simple example, like our car array, if we want to repopulate the array, we can simply replace the value with the payload from the action. However, if we implement the addBikes action as well, watch what happens with this simple solution — when we create a new bikes object, the cars disappear! We need to make a copy of the current state, and then change the bikes object, leaving the cars as is. Creating a shallow copy can be accomplished using the spread operator (…state), calling Object.assign() in the addBikes reducer:
return { …state, bikes: action.payload} //ORreturn Object.assign({}, state, {bikes: action.payload})
Try it out. They both make a copy of state and then replace the empty bikes array with the data.
Things get a bit more complicated when trying to add an element to one of our arrays, say the cars. We need to copy the entire state, and then add the new car to the copied state’s car array:
case ‘ADD_CAR’:
return {…state, cars: […state.cars.concat(action.payload)]}
case ‘ADD_BIKE’:
return Object.assign({}, state, {bikes: state.bikes.concat(action.payload)})
Once again the copy can be made using either the spread operator or Object.assign(). The new element is added to the array using .concat(). You might think to use .push() to add the element, but it returns the length instead of changing the array (try it out).
So far we’ve looked at adding items to our Redux store. I’ll leave it to you to discover similar strategies for removing and changing Redux store’s immutable state. I encourage you to clone my Redux playground, or better yet, set up your own. The more you play, the more comfortable you’ll become using Redux. It’ll be worth the while.