Using react-redux and legacy_createStore
The example in this post can be viewed at https://codesandbox.io/s/animalzoo-5dn5p3
Redux and React using legacy_createStore
Redux is a marvelous tool but it doesn’t exactly fit well into JavaScript. It expects immutability, but JavaScript objects are mutable. It wants each action to have a type (in fact, each action object has key called type), but JavaScript is only weakly typed. It lives in a land where functions are pure—which can be a nice place to write software. You just need to be very careful when doing it in JavaScript. Luckily, you can separate almost all of your Redux logic into its own, nicely testable slice of programming land and let it grow and develop, isolated from the rest of your application. We just have to learn a few rules, and can we have all the things that Redux promises. legacy_createStore used to be called createStore. The new Redux API has a different way of creating store. It’s laid out in such a way that you can follow the same best-practices of the old Redux API, but you won’t have to worry so much about things like pure functions and mutating state. Still, the new Redux API follows the old Redux API closely and it’s helpful to learn how to use legacy_createStore so you can understand and appreciate why the new Redux API made the choices it did.
Installation
We just need two dependencies: react-redux and redux.
npx create-create-app ReduxDemoZoo
npm i react-redux redux
Basic ideas
Our app needs to keep state. Redux will keep this state in a store.
The store object will come out of the store as a simple JavaScript object. When it comes out, it will be very simple and there are no tricks to worry about there. Putting data into the store is were we need to focus our attention.
Reducers
Let’s make a way to put an animal into the Redux store for our zoo.
// store/reducer.js
const initialState = {
animals: [],
zooKeepers: [{ name: "Prof. Elm" }],
};
const reducer = (state = initialState, action) => {
// if called with no arguments this will return the initialState
if (!action) return state;
switch (action.type) {
case "ADD_ANIMAL":
return {
...state,
animals: [...state.animals, action.payload.animal],
};
default:
return state;
}
};
export default reducer;
The crux of redux is the reducer. The reducer lives in pure function land, so there are a few rules to be sure to follow. The most important rule is to not mutate state. This means you may not change the instance variables on state. Instead, we can use the … spread syntax to de-structure and re-create objects and arrays. (In the new Redux API, we don’t need to use the … spread syntax anymore, but it’s still important to understand why we need it here.)
Let’s look at the above reducer function bit-by-bit. First, the initialState. When we write (state = initialState, action), we are setting state to the initial state by default. This is required so Redux can set up the initial state of the store by calling reducer() with no arguments. It’s a clever little part of the Redux API.
Next, let’s look at action and the switch statement. Each action must have a type. We can make up our own types as we go. In this case, I need an action that adds an animal to the zoo. I will call this ADD_ANIMAL. I have decided that the payload of the state will be a JavaScript object with this shape:
payload = { animal: { species: "lion" } };
The action will have this shape:
action = { type: "ADD_ANIMAL", payload };
The result of the reduce function will be to return a new state object with one difference from the old state object. That is: the array, animals, will now include the new object: {species: 'lion'}.
We can add more case statements to deal with as many action types as we need. One thing to remember, all the state changes will happen in the reducer function. And all of these changes ought not mutate state. Instead they all create a new state object that is different from the old state object.
Once you can understand the idea of a reducer, explained above, the rest is just boilerplate additions to connect everything together.
Actions
To create a new state, we could just call…
const newState = reducer(oldState, {type: 'ADD_ANIMAL', {animal: {species: 'lion'}})
…but then we would have to worry about misspelling the type or getting the payload wrong. Instead, we can create a function that will create the action for us. We can just call these functions “actions” and we will need to decide on what arguments they will take. This part of the API is ours to create.
// store/actions.js
export function addAnimal(species) {
return {
type: "ADD_ANIMAL",
animal: { species: species },
};
}
Now we have a way to update the store and a way to get the store.
Testing reducers
Now we can test a reducer like this:
// store/reducer.test.js
import reducer from "./reducer";
import { addAnimal } from "./actions";
describe("Reducer", () => {
test("should add animal to zoo", () => {
// determine initialState
const initialState = reducer();
// determine expected state for test
const animal = { species: "lion" };
const expectedState = {
...initialState,
animals: [...initialState.animals, animal],
};
// run update on state, and confirm that it has been updated.
const action = addAnimal("lion");
expect(reducer(initialState, action)).toEqual(expectedState);
});
});
Notice that testing the reducer requires no other dependencies. That’s because the reducer is just a pure function.
Create a store with legacy_createStore
With the reducer that we just wrote above, Redux can create a store for our app. Here is where we use legacy_createStore.
// store/index.js
// import { createStore } from 'redux';
import { legacy_createStore } from "redux";
import reducer from "./reducer";
// let Redux create a store from our reducer.
const store = legacy_createStore(reducer);
// NOTE: createStore is equivalent to legacy_createStore
// const store = createStore(reducer);
export default store;
Use Redux with a Component
Our app needs to access the store, so we can use connect and let react-redux connect it to our Component.
// components/animal.js
import {connect} from 'react-redux';
function Zoo(props) {
return (
<div>
{props.animals.map( (animal, idx) =>
<div key={idx}>
{animal.species}
</div>
)}
</div>
)
}
const mapStateToProps = (state) => {
return {
animals: state.animals
}
}
export connect(mapStateToProps)(Zoo);
Now the Component will have access to the animals value from the redux state object. The Component will be kept in sync with updates to the store.
Dispatch
Finally, we need a way for Components to update the Redux store.
Suppose we wanted to add a WildAnimal to the Zoo.
// components/WildAnimal.jsx
import { addAnimal } from './actions';
function WildAnimal(props) {
const dispatch = props.dispatch();
const handleClick = () => {
const species = props.species; // = 'tiger'
const action = addAnimal(species);
// = {
// type: 'ADD_ANIMAL',
// payload: {animal: {species: 'tiger'}}
// }
dispatch(action);
}
return (
<div onClick={handleClick}>
{ props.species }
</div>
)
}
export connect()(WildAnimal);
All we need to is import our action, addAnimal, which produces the Redux formatted action. Then we pass that to dispatch, which is set up by Redux as a prop when we connect() our Component. WildAnimal doesn’t need state, so there is no need for mapStateToProps. Finally, we will need to be sure that this Component under our Redux Provider, as described next.
Putting it all together
Now, the Zoo has props connected to the store with connect from react-redux. WildAnimal has access to the dispatch function from react-redux through useDispatch. They both need to be below a Provider from react-redux.
// App.js
import { Provider } from "react-redux";
import store from "./store";
import Zoo from "./components/Zoo";
import WildAnimal from "./components/WildAnimal";
function App(props) {
return (
<Provider store={store}>
<div>
<h1>Wild Animals</h1>
<WildAnimal species="lion" />
<WildAnimal species="tiger" />
<WildAnimal species="bear" />
</div>
<div>
<h1>Zoo</h1>
<Zoo />
</div>
</Provider>
);
}
Now when you click on a WildAnimal it will be added to the Zoo. The flow will look like this:
graph TB
A["click on Component"]-->B["Inside Component, call dispatch with action"]-->C["reducer function is called with action as input"]-->D["reducer function returns updated state"]-->E["Redux updates all connected components"]
You can add other components that use this action. You can create other actions with a corresponding case statement in the reducer function. You can also access all the features of react-redux like dev tools and middleware. Once you have your application state sealed off in pure function land with Redux, there are a lot of tools you can use to debug, maintain, and enhance your store.
Why is it called legacy_createStore?
The Redux team has come up with a new way of managing state that is less error prone. That is, you won’t have take so much care to avoid mutating state. It will help you to avoid state errors by making it almost impossible to change state in the “wrong” way within a Redux reducer. For myself, it makes more sense to understand the “legacy” way to do it first, then to dive into the “new and improved way” with an understanding of where the ideas are coming from. So, I’ll write about that in an upcoming post.
This is the first in a three part series. Next week, I will write about how to put together multiple reducers. And the following week, I’ll write about how the new Redux API deals with the same problems, but without worrying about mutability.
The example in this post can be viewed at https://codesandbox.io/s/animalzoo-5dn5p3