What exactly is Redux, anyway?
There's not a whole lot to it. But as it turns out, that's also what makes it so great.
It embraces a hard-learned lesson:
State management is the hardest problem in building UI applications.
Whatever, Henrik. Be less abstract!
Ok, so what I mean by "state" is what mode the app is in right now: what should the user be looking at, what data are we fetching, what items has the user selected, what's the URL pathname the browser is showing, are there errors, etc.
If you distilled these things down to the simplest possible JavaScript object, what information would this state
object need to contain? React, and similar rendering approaches let us conceptually dumb-down the entire application into the following bit of pseudo-code:
UI = view(state)
Assume that view()
is a function that knows how to update the DOM and, ultimately the browser as a whole, to show the user what they should be seeing given the current state
. Whatever this state
argument would have to be when you pass it to view()
is the "application state."
You can think of it as a normalized database maintained as a JavaScript Object if that helps.
Can you be even less abstract?
Let's say we're building an incredibly simplified clone of Google Image Search that searches Flickr where users can enter a search string and retrieve a list of images.
Our application state's structure could look something like this:
state = {
query: 'puppies',
fetching: false,
results: [
{ id: 234, url: 'https://cute.com/puppy.png' },
{ id: 67, url: 'https://cute.com/pug.png' },
{ id: 23423, url: 'https://cute.com/silly-puppy.png' }
]
}
This object contains the minimum amount of data we need, to represent the state of our image search application.
If you think about it, by extracting our state out like this, we're just separating concerns. Our view doesn't have to handle tracking state. It only needs to know how to show the possible variations of this state.
Let's examine some specific scenarios and then show a potential implementation of the view()
function that returns the DOM we expect for a given state.
Basic view implementation
If we think about how our app should behave, we probably want a form at the top that is always visible so that the user can update the search query at any time, even if the app is currently loading other stuff.
I'll be using JSX and assuming React (or something similar) for the examples, but as I mentioned the actual rendering of the UI is an entirely separate concern from our application state, so it could be anything capable of updating the DOM.
The basic structure will be as follows:
<div>
<form>
<label htmlFor="search">What do you want to search for?</label>
<input id="search" type="text" name="search" />
<button type="submit">Search</button>
</form>
// this is the interesting bit
<Content state={state} />
</div>
Scenario #1: App boot
Our state would probably be as follows when someone first opens the app:
state = {
query: '',
fetching: false,
results: []
}
Things to note:
- We don't have a query yet
- We're not currently fetching anything
- We don't have any results
Based on that knowledge, we know our view should show a sizeable empty text input with a message prompting the user to search. As it turns out, this is what we just said we were going to always have at the top of the page.
So, in our <Content />
component all we have to do in this state is return null
which will cause nothing at all to be rendered for this component:
const Content = ({ state }) => {
if (!state.query && !state.fetching) {
return null
}
// ... more cases to come
}
Scenario #2: User runs a search
Let's assume the user now types in "puppies" and hits "Enter." Our app state would now be this:
state = {
query: 'puppies',
fetching: true,
results: []
}
At this point, our view should tell the user that the app is in the process of fetching results for the search term they entered.
- The query can be rendered by our view as part of a loading message: "Searching for puppies..."
- The
fetching: true
flag is sufficient to tell us that a search is in progress.
const Content = ({ state }) => {
if (!state.query && !state.fetching) {
return null
}
if (state.fetching) {
return <p>Searching for images of {state.query}...</p>
}
// ... more to come
}
Scenario #3: Search results come back
state = {
query: 'puppies',
fetching: false,
results: [
{ id: 234, url: 'https://cute.com/puppy.png' },
{ id: 67, url: 'https://cute.com/pug.png' },
{ id: 23423, url: 'https://cute.com/silly-puppy.png' }
]
}
- We have an array of results
- fetching is now
false
The view can now iterate through the results to show them in a list.
const Content = ({ state }) => {
if (!state.query && !state.fetching) {
return null
}
if (state.fetching) {
return <p>Searching for images of {state.query}...</p>
}
if (state.results.length) {
return (
<ul>
{state.results.map(result => (
<li>
<img src={result.url} />
</li>
))}
</ul>
)
}
// ... more to come
}
Scenario #4: Search query returns no results
state = {
query: 'puppies',
fetching: false,
results: []
}
- there is a query
- we're not currently fetching
- but there are no results
We know that our view should be showing a message saying there were no results.
const Content = ({ state }) => {
if (!state.query && !state.fetching) {
return null
}
if (state.fetching) {
return <p>Searching for images of {state.query}...</p>
}
if (state.results.length) {
return (
<ul>
{state.results.map(result => (
<li>
<img src={result.url} />
</li>
))}
</ul>
)
}
// if we get this far the search is done but we have no results
// so we just show a message saying no results
return <p>No images found for {state.query}</p>
}
Is it really that simple?
YES! The basic idea is to just isolate application state from your view layer. So whenever someone talks about "state" in Redux, remember this simple example. Now that we have a clear understanding of what we mean by application state, you may be wondering what this has to do with Redux.
Well, the official documentation describes Redux as follows:
Redux is a predictable state container for JavaScript apps.
So, Redux will serve as a container for our application state, and somehow "make it predictable." Hmm, OK. We'll get to that, but for now, let's think through what we'd need to do to build an app that kept all its state in the way we just described. We'd need to do something like this:
- Define our initial application state when the app starts.
- When something happens, we need to update our application state accordingly.
- We re-render our view with the new state.
As it turns out, none of this requires Redux at all. We can certainly build an entire app using this basic pattern without Redux. For example, if we're using a library like React or Preact to update the DOM, we could keep our application state in a root component that we call "App." If you're not familiar with React, a React component can store an object of local state
. Then, whenever you want to change its state, you can call this.setState()
with your changes. In turn, this causes its render
method to rerun and update the DOM. If you're not familiar with Preact, you can think of it as a "React Lite." It implements nearly the same API and is only 3.5kb minified and gzipped, which about 1/10th the size of React.
You can see a runnable and editable version of this app by visiting: https://reduxbook.com/image-search-simple
If our app were as simple as what we've described, there'd be no need for Redux at all. But often we want to do a lot more than this, and maintaining all your application-related code inside a component can become unwieldy. But let me be clear on this point: if your app is simple enough to not need it, don't make things any harder than they need to be.
But, as an application grows and gains features, we often find ourselves in a scenario where a component needs to access state contained in an entirely different one. At this point, Redux can help. Instead of using component state, we can create something that lives outside our components, to store application state. At this point, we can make it available to any of the components in our app.
Redux is simply that: a pattern for storing, updating, and reading application state outside of your components.
So how does Redux store our state?
When using Redux, we create a single object called a "store" that, ahem... stores our application state. We create a store by calling the createStore()
function exported by the Redux library. This function returns a plain old JavaScript Object
with only four methods attached to it! When I say there isn't all that much to Redux, I mean it.
If we have any initial state that we'd like to start with, we can also pass that to createStore()
when we first create the store. Then whenever we need the current application state, we simply call .getState()
on the store. It takes no arguments and simply returns the current state.
So in essence, we start out with some state, then things happen that cause changes to the state, and we retrieve the state via .getState()
whenever we need it. I think it's important to realize just how simple the basic concept of Redux is. Understanding this will make everything else seem less intimidating.
You may be wondering, if it's so simple then why do we need Redux at all?
- It provides a set of patterns and conventions for building applications that work this way.
- Having a single object containing all of our application state can become unwieldy as it grows. Redux provides a way to slice this state into more manageable chunks.
- Redux offers a structured approach for how to go about making updates to state.
For simple apps, like our photo search example, it's easy to keep the entire state we need to track in our mind. But for real applications, especially applications being worked on with a team, it quickly becomes unreasonable to expect everyone to know how to manage the entire state object. We have to find a way to slice up that state and handle updates to parts of it without worrying about how it might affect another part of the app.
For this, we're going to learn about actions and reducers.
Chapter recap
- State management is the hardest problem when building UI applications.
- Application state is a minimal representation of what mode the app is in right now.
- We built a simple image search application without Redux that uses the idea of extracted application state.
- The accompanying example code is here: https://reduxbook.com/image-search-simple
- Redux is a structured approach for storing and updating our application state.
- The official Redux API documentation is here: https://redux.js.org/api-reference