Let me set the stage: you’ve come up with a great idea for a web app, and you’ve decided to build it with React. By way of completely arbitrary example, let’s say it’s a guitar chord finder with an interactive guitar neck and a submission form (like, say, this one).
Shameless self-promotion!
You’ve made a couple to-do lists, watched a couple tutorials, and you think you’ve got the hang of all this “props” stuff, so you jump right in. It’s going well enough — you’ve got your top level app component, maybe a form, a guitar component… and then, it starts to dawn on you. You’re going to need a guitar… with six strings… with twelve frets each… which all need to communicate information about whether or not they’re muted, pressed, or open…which need to be able to communicate with the form and express a chord… Before you know it, six hours have passed, there’s yarn all over the walls, and you’re pretty sure you’ve uncovered a conspiracy inexplicably implicating completely fictional people and the United States Postal Service.
Side note for Always Sunny fans, did you ever notice that “Pepe Sylvia” sounds an awful lot like how an illiterate person would try to read “Pennsylvania”? That’s the sort of subtle comedy we need.
But fret not (pun intended)! There’s this cool tool called Redux you may have heard about, and it’s designed for scenarios just like this, when your component nesting is out of control and props are being passed around left and right. It’s arguably on the way out, thanks to React’s newly retooled Context API and other competing libraries, and it tends to stir up passionate feelings among developers who tend to alternatively think it’s either the best thing since sliced bread or an unholy obfuscating mess, but it definitely still has its place.
The golden rule regarding Redux’s use, from its creators and devs the world over, is something approximating “If you’re not sure if you need Redux, you probably don’t.” To those people I say… well, you’re probably right. Redux does add a lot of complexity and boilerplate for simple tasks, which we’ll soon find out. And, as a matter of fact, I did build this guitar app with regular old React, and it worked out fine. But, I was young and naive then (two months ago), so for explanation’s sake, let’s find out how it could have made our lives easier. Once we get the boilerplate out of the way, Redux has the ability to make your apps more scalable, more performant, and easier to debug and reason about. Plus, I really like it!
An artist’s rendering of a React app. Who’s got time for all that nesting?
So let’s assume you’ve got a React app all built up. If not, check out one of the many great tutorials out there. Even try create-react-app if you want. Once you’re up and running, the first thing we’ll want to do is install our packages!
sudo npm install reduxsudo npm install react-redux
Astute readers may note that we’re installing both Redux and a React-specific version — what gives?! Well, Redux is actually a framework-agnostic library. You can use it with Backbone, or Mithril, or Angular, or Meteor, or Vue — it’ll work with all of them! The React specific version, though, has a few methods which will become very useful to us when we’re setting things up in our components.
With installations out of the way, we’re going to set up our file structure, which has a few more pieces than what we’re used to in a traditional React app. Everyone does this a little bit differently, but typically I like to make an actions folder, a reducers folder, and a store folder inside my react-client/src directory, like so:
You may see some folks use constants, I don’t bother with those.
Great! So let’s begin with the store — this is what makes Redux work. You can think of it as an external state container which React components can directly interact with by means of a few special methods, regardless of their level of nesting in a project’s hierarchy. Need some state? Go to the store! Need to update the state? Dispatch an action to the store! Everything is connected!
That’s right, I used the same reference twice in one article. It’s… a metaphor for how Redux makes you write the same thing in a few places in your app… or something.
Let’s make an index.js file inside of our store folder. First, we’ll import React, so we have access to it — makes sense. Next, we’ll import two methods directly from Redux: createStore and combineReducers. These will do just what they sound like! What are reducers and why are we combining them, though? Let’s put that question on hold, except to say we’re also going to import our reducers.js file, which we’ve yet to make in our reducers folder. After all our import statements, we’re going to create a variable called store and combine our reducers inside of it with combineReducers, which takes an object as an argument and will become our global application state_._ Put it all together, and it looks like the below.
Line 9 is exclusively for using Redux’s excellent browser extension. I highly recommend it, but it’s not mandatory.
There is still one missing piece: Redux’s Provider component. This is a context wrapper which needs to surround your entire application and which receives the store as a property, allowing all of your other components to have access to it via Redux’s methods. At the very highest level in our app, our index.jsx file where we’re attaching React to the DOM, we’ll simply import the Store, the Provider, and our App component (which we’ll build in a second) and bundle everything up, like so.
This way, our entire application has access to our store!
Now that we have our store set up, let’s talk about the rest of the Redux ecosystem. This is the most complicated part so bear with me. Effectively, you have a circular relationship that goes Store -> Components -> Actions -> Reducers -> Store (seen below in diagram form).
The store’s state is mapped onto a component by way of a little function called mapStateToProps — you can think of this as a subscription service. In fact, the native Redux method this utilizes is called subscribe (even though we won’t actually be using it directly in this tutorial). Any change made to the store will flow down to any component which has subscribed to that property in the store, triggering a re-render. Neat, huh?
In order to actually affect the state of the app, however, a component has to use a separate function: mapDispatchToProps. Dispatch is another store method, which takes an action (which we import) and sends it off to a reducer, which receives that action and actually changes the state of the app. I know that’s a mouthful, but let’s take it in steps, starting with a super-simple component. This will be our highest level App component, and it will also import a method known as bindActionCreators from Redux as well as a method called connect from React-Redux. The former just makes writing our mapDispatchToProps function a little cleaner, while the latter is a higher-order component which bundles our component up with mapStateToProps and mapDispatchToProps so that these methods have access to the store. To wit:
Notice how we’re importing our action on line 5_._
There are a couple other small points of order here — notice how mapStateToProps takes in the application state as a parameter, and how we pluck out individual properties from that state. These properties will be accessible on that component’s props, so if we wanted to get examplePropOne, we would do it like so in our component’s render function (note the <p> tag):
render() {return (<div><h1>Hello world, this is a Redux tutorial!</h1><p>Here is our property: {this.props.examplePropOne}</p></div>)}
This way, any changes made to examplePropOne, even if from another component, will flow through and be rendered in this component. You don’t have to pick out all the properties from the state object in your mapStateToProps function — only bring in the ones your component cares about!
mapDispatchToProps, similarly, takes in dispatch as a parameter. Dispatch is a special method of the Redux store, and by associating our action with it through bindActionCreators, we are both attaching it to the component’s props and saying that, whenever it’s invoked, it will dispatch the action in question to the reducer to be ultimately turned into state in the store.
Here’s an example of how that might look in a function on the same component:
exampleFunction() {this.props.exampleAction();}
Still with me? Last thing to note in the component is the syntax of the connect method — it will always take two arguments, and they will always be mapStateToProps and mapDispatchToProps, in that order. You can omit mapDispatchToProps easily, but if you want to omit mapStateToProps on a specific component, be sure to leave a null value in its place. The name of the component itself should be invoked at the very end of the connect statement. Here’s another example, this time written without mapStateToProps and with a component called List:
export default connect(null, mapDispatchToProps)(List);
So far we’ve taken care of the store and component parts of the Redux life-cycle. That just leaves actions and reducers.
Actions are the easy part — they’re just functions that create JavaScript objects, with one compulsory property called type. By convention, type is typically written in all caps and is very descriptive of what that action actually does. It often has another property called payload, in which we put any actual data being passed into the action. In general, we want to avoid including any actual application logic inside of these actions: they should simply deliver a type and potentially some data. Here’s how our actions/actions.js file might look:
Notice how the second action receives an argument, which it passes along as a payload.
That’s really all there is to actions!
At this point, we only have one item left in our life-cycle: reducers. A reducer is effectively a giant switch statement that takes in an action and, depending on its type, updates the application’s state. If you remember, when we created our store above, we imported our reducers and passed them through combineReducers in our createStore function, meaning that whatever returns out of our reducers will update the state of the store. Those changes will then flow down through any mapStateToProps functions that we put in our components to subscribe to the store, and our data will be fully linked. That’s Redux! We did it!
Now, there is one important concept I’ve left out so far — indeed, it’s one of the most important, foundational concepts in Redux: immutability. In a nutshell, with Redux, for the purposes of easier debugging, clean, singleton code, and a more performant application, we never actually directly mutate the application’s state. Instead, every single state change is recorded as a snapshot in time, and every change is made to a copied version of the state. For a most obvious example of how this is useful, take the Redux dev tools alluded to earlier: it keeps a log of the entire of history of state in the entire app’s life-cycle so that you can see step-by-step how and when things are changing. Super useful! I highly recommend poking around with it if you have the time.
All that being said, here’s an example of what a reducer might look like — that is, a function which takes in the application’s state, as well as incoming actions, runs it through a switch statement, and returns the updated version of the state to deliver to the store:
We do not have to provide the default state as I did in this example—in some apps, you will define a default state in a different file, give it initializing properties, and then import it into your reducer. In other apps, you might simply initialize the state as an empty object using ES6’s default parameter. There are no hard and fast rules to initializing state except that it must be defined, and your reducer must always return a state. For that purpose, it’s also very important that we don’t forget our default case on line 18.
Crucially, the reducer should take in a state object and an action as arguments, set up a switch on the action’s type property, and then return a copied version of the state with the desired updated properties. Note the spread operators on lines 10 and 15: these spread the previous state before adding and/or modifying the desired properties, meaning you’ll end up returning a copied, slightly modified version of the state to the store with every action. You could also do this using Object.assign, but I find the spread operator more elegant. Note also how on line 16 we’re using the action’s payload property to deliver data into our state, whereas on line 11 we’re hard-coding a modification to examplePropOne.
There’s a fair bit more we could talk about when it comes to React-Redux. Take, for example, asynchronous actions: passing around the results of API calls, operations which most of the time are pretty painless, can potentially cause a headache if you have actions that require certain criteria to be met before executing. There’s a library called redux-thunk to help deal with that. You can also have multiple reducers in one project, which you would then pass in through the combineReducers function in your store file. Additionally, local state can still be really useful in individual components, and you should really only involve the store in cases where you have state being passed around between nested components. I encourage you to read more and start experimenting to learn all the finer points of Redux, and to read about competing state management solutions including mobx and the aforementioned Context API.
At this point, though, I hope you can see how Redux might make our guitar app a bit simpler. Say a user clicks on a fret, and we need to update the string it’s on, the guitar component, the app component, and the form component. Instead of passing around props all over the place, through multiple levels where it may not even be used, we can instead dispatch a single action and set up a couple subscriptions and we’re good to go! In especially large apps with a lot of nesting and a lot of state to manage, this can make your life a lot simpler, and is one possible solution to a very sticky problem.
If you made it this far, thanks for reading! Here is a cute ferret as a thank you.