Actions and reducers: updating state
I glossed over how state changes occur in the previous chapter. Let's look a bit deeper.
In Redux all changes to the application state happen via an "action." An "action" in Redux terms is simply "a thing that happened" expressed as a plain JavaScript Object
. For example:
{
type: 'LOGIN_SUCCEEDED'
}
It can be helpful to think of actions as "reports" or news stories. They don't describe intent, they describe facts that happened.
In Redux we say that actions (reports) are "dispatched" to the store to let it know which things happened. As it turns out, there's a .dispatch()
method on the store for just this purpose. So to report to the store that a login attempt was successful you could simply dispatch the 'LOGIN_SUCCEEDED'
action directly on the store, like so:
store.dispatch({ type: 'LOGIN_SUCCEEDED' })
What you choose as the name of the action sent as the type
property can be anything. However, it's a subtle but important thing to realize that actions should be used to report a thing that happened, not cause something to happen. Again, think of them as news stories: they don't tell you how to react, they inform you of what occurred. We'll see how these reports are consumed later when we discuss reducers.
One little "hack" to keep yourself writing actions this way is to make all your action types past-tense. So instead of calling an action LOGIN_SUCCESS
, you'd call it LOGIN_SUCCEEDED
. Doing that can help you avoid a few common anti-patterns down the road. The concept of reporting things that happened feels a bit foreign at first. But if we consider something asynchronous like fetching data from a server, it makes a lot more sense. There isn't just one action like FETCH_USERS
. Sure, your app may be fetching users, but several different things occur as part of that:
- Certainly, there will be some function you call that initiates fetching of data. But first, the data fetch is initiated, which may be newsworthy to the rest of the app:
{ type: 'FETCH_USERS_STARTED' }
- Then, if the fetch was successful, you'll get data back from the server. But we don't want to send useless "reports" full of irrelevant information. So even though the server response may include all sorts of other stuff, you should distill the response down to just the relevant, parsed portion that matters to the application, and report that. I mean... ahem, dispatch an action:
{ type: 'FETCH_USERS_SUCCEEDED', payload: [{ id: '1', name: 'Mary' }, { id: '2', name: 'Jane' }] }
- Alternately, there could be something that goes wrong, like not being authorized to make a request, or the mobile device being offline, in which case we may report what went wrong like so:
{ type: 'FETCH_USERS_FAILED', payload: 'connectionFailed' }
This point is pivotal to getting the most out of Redux: the recepient, not the sender determines how a given report affects the state. If you find yourself creating actions that sound like a a function call like START_LOGIN
, or a setter like SET_SIGNED_IN_TRUE
you're likely missing the point.
Actions are like news reports. Only report what matters; only report the facts.
Just like our application state is made up of plain objects, the actions are also plain objects. Are you noticing a trend yet?
As it turns out, the vast majority of code you write using Redux are plain JavaScript functions that operate on plain JavaScript objects and arrays. There are no fancy classes we inherit from, or base models we extend. Instead, we write lots of functions and combine them into an app using utilities provided by Redux. The nice aspect of this is that there isn't much of an API to learn. Remember, the store object only has four methods out of the box!
Anyway, as we said, an "action" is just a plain JavaScript object. The only other limitation that Redux imposes on actions is that they must have a type
property. But additionally, the action should be self-contained (or put differently the action should be atomic). What this means is that it must also include any related information required to report the "truth" that just occurred fully. Often this is stuff like the ID of an item that was selected, the data that was just returned from a server call, etc. By convention, I always put these types of values in a property of the action called payload
. Wikipedia defines "payload" in computing to mean:
the part of transmitted data that is the actual intended message.
source>)
I like this because it's self-descriptive, but to be clear, you do not have to follow this convention. The Redux documentation on actions, for example, does not. It contains this example:
{
type: 'ADD_TODO',
text: 'Build my first Redux app'
}
When I first started using Redux, I didn't name the payload something consistent. I would call it something descriptive like text
as seen in the example above. But, I found myself frequently forgetting what I had called it when working on a reducer that was handling that payload, then having to look it up, or worse, using the wrong name then wondering why my app wasn't working. As I said in the introduction, the longer I've worked as a programmer, the more I've come to appreciate patterns and tools that reduce surface area for mistakes. So, personally, I now always call it payload
.
So anyway, as an example, an action that occurs when we successfully fetched an authentication token may look like this:
{ type: 'AUTH_TOKEN_RECEIVED', payload: 'eyJpZCI6Ii1LaF9oxzNzMwfQ' }
Some parts of our app may only need to know that we have a token at all, while another part of the app may need the token value itself so it can be passed along to an API call, etc. But either way, the action is a self-contained, atomic thing that includes all the relevant information that goes along with it.
By convention for the type
, we use uppercase strings with _
instead of spaces. So instead of writing "user logged in"
like a normal human being, we'd write "USER_LOGGED_IN"
so that we can pretend we're robots. Why you ask?? Well, ahem... Redux certainly doesn't care, as long as it's a unique string. But following these conventions will make it easier for other folks who are familiar with Redux to make sense of your code. It's also a common practice in JavaScript itself to describe constants as uppercase strings.
On the topic of action constants, a lot of folks when using Redux will create an action type constant once and then import that constant wherever it might be needed. This approach is also entirely optional, but is preferred by some people, for the same reason as calling the payload "payload
." It decreases the risk that you'll do something like dispatch an action of type 'LOG_IN'
in one place, but in the code that processes that action, type 'LOGGED_IN'
. If instead you define the action constant for a given type once and import it when needed, this won't happen. Because if you mistype the variable name, your program will break in obvious ways because you're importing things that don't exist. The choice is yours. Personally, I often find it a bit too laborious to have to import and export action types, but do as you wish.
Speaking of the code that uses the action to update our application state, in Redux terminology this part is called a "reducer."
Reducers
So if an action is a "news report" a reducer is a person reading the report and choosing if they want to change anything about themselves in response. How, and if that person changes is entirely up to them. Similarly, how and if a reducer updates the application state in response to an action is entirely up to the reducer. Putting this responsibility on the receiver of the action provides a clear separation of concerns, again reinforcing that dispatching an action is not just a fancy, abstracted function call. This is important because several reducers can update state from a single action.
So what is a reducer in Redux terms? It's a plain old function you write that takes the current state, and the action to be processed, and returns the state as it should be, based on that action occurring.
If you'll recall, store.getState()
just returns the entire application state. So when we call the store.getState()
method after a dispatch, we'd get the updated version of the application state, with all updates applied.
The name "reducer" is a bit confusing because it doesn't "reduce" multiple values down to one. Calling it a "state updater" would perhaps have been less confusing, but the name was chosen for a good reason: this function's signature matches the signature of the function required when using the .reduce()
method on an Array
.
For example:
const numbers = [1, 2, 3, 4]
numbers.reduce((total, number) => {
return number + total
}, 0)
Note that the function signature is: (state, itemToProcess) => newState
.
Similarly, in Redux a reducer takes the starting state and an item to process, and return the new state. But the itemToProcess is the action!
For the time being let's write a reducer (or, ahem... "state updater function") that only keeps track of the current search term in our image search example from the previous chapter.
// state defaults to '' to start
const reducer = (state = '', action) => {
// handle a particular action type
if (action.type === 'START_SEARCH') {
// return the new state
return action.payload
}
// always return state
return state
}
Note that just like the function we first passed to Array.prototype.reduce
when reducing an array, our reducer function takes a starting state and returns either the unchanged state
, or a new state object if an action occurred that should cause a change in the state. However, the function signature is where the similarities end.
Reducers in Redux have a few simple rules:
- Always return the state, even if you didn't change it, and even if it's just
null
. You may not returnundefined
. - If you change state at all, replace it (more on this later).
- Every reducer processes every action even if it doesn't do anything with it.
We'll examine these more closely here shortly, but first, you should understand how a reducer is related to the "store" we mentioned before.
As it turns out, the Redux store only takes one reducer. In fact, a reducer is the first and only argument that createStore
requires to create a store. A lot of folks seem to miss this at first because they see that we usually end up writing many reducers to handle various parts of our state. But we can only pass Redux a single reducer when calling createStore()
to set up our store. The reducer we pass to the store is often called the "root reducer."
import { createStore } from 'redux'
const reducer = (state, action) => {
if (action.type === 'LOGGED_IN') {
return {
loggedIn: true
}
}
return state
}
const store = createStore(reducer)
So how do we end up splitting our state into smaller, more manageable pieces if we can only supply a single reducer to Redux?
Enter combineReducers()
The combineReducers
function is a helper included with the Redux library. It takes an object where each key
is a name, each value is a reducer function, and it returns a single reducer that combines them all into a single reducer. So the resulting function takes the whole starting application state, the action to be processed, and returns the new application state. But it does this by splitting up the state object by keys and passing only that slice of the state to the individual reducers.
In this way, instead of having to handle all our state changes in a single, massive reducer that handles every action type our app may ever use, we can combine several smaller reducer functions into a single one.
Let's look at an example:
import { combineReducers, createStore } from 'redux'
// reducer #1
const floodCountReducer = (state = 0, action) => {
// handle action types here...
if (action.type === 'APARTMENT_FLOODED') {
return state + 1
}
return state
}
// reducer #2
const initialFurnitureState = { hasFurniture: true }
const furnitureReducer = (state = initialFurnitureState, action) => {
// handle action types here...
if (action.type === 'APARTMENT_FLOODED') {
return {
hasFurniture: false // crap, the flood ruined our furniture!
}
}
if (action.type === 'BOUGHT_FURNITURE') {
return {
hasFurniture: true
}
}
return state
}
// now we combine them into a single root reducer!
const rootReducer = combineReducers({
floodCount: floodCountReducer,
furniture: furnitureReducer
})
// we end up with a single reducer we can pass to `createStore()`
const store = createStore(rootReducer)
The core idea of combineReducers()
is simple:
- For each
key
in the passed object, we'll end up with a correspondingkey
in the application state. So if you calledstore.getState()
immediately after creating the store in the example above, the state would look like this:{ floodCount: 0, furniture: { hasFurniture: true } }
- When an action is being processed by the root reducer (the function returned by
combineReducers()
) rather than passing the entire application state to each reducer, it only passes the relevant slice. So, if you were toconsole.log(state)
inside thefurnitureReducer
above, you'd only get:{ hasFurniture: false }
There would be no way for you to change the floodCount
state from inside the furniture reducer because that part of the state is never made available to it! In this way, you can slice up what could potentially be a huge application state object into smaller, more manageable pieces.
If you look at the source code for combineReducers
on GitHub, there's a lot of code in there, but the vast majority of it is just to help developers catch errors. As a learning exercise, we can re-implement combineReducers
. To do this, we will write a function that returns another function. The arguments passed to this function will determine how that returned function works. We're getting into Functional Programming patterns, which we'll see a lot of, in Redux.
Let's see what an implementation of combineReducers
could look like:
function combineReducers(reducers) {
// grab all the keys from the object passed in
const reducerKeys = Object.keys(reducers)
// return a function that has the same signature as all reducers do
// (state, action) => state
return function combination(state = {}, action) {
// a flag to track if the action has caused any changes at all
let hasChanged = false
const nextState = {}
// loop through each reducer that was passed in
reducerKeys.forEach(key => {
const reducer = reducers[key]
// grab the slice of the whole state that is
// relevant for this reducer, again based on its key
const previousStateForKey = state[key]
// actually run that slice through the reducer for this key
const nextStateForKey = reducer(previousStateForKey, action)
// tack it onto the object we'll ultimately return
nextState[key] = nextStateForKey
// keep track of whether or not anything actually changed
// but only keeps this check going if we don't already know
// something has changed.
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
})
// return the unchanged or the new state
return hasChanged ? nextState : state
}
}
This idea of being able to slice up and handle state updates for a portion of the whole app is, in my opinion, one of the more ingenious aspects of Redux.
If you're thinking ahead, you may have noticed that this helper function isn't just useful at the root level. It can be quite helpful to segment your state even further, and combineReducers()
lets you do that easily. So even though you probably wouldn't combine all reducers in a single place like this in a real application, the following would work fine:
// NOTE: Don't write code like this. It's for illustrative purposes
// it makes more sense to import the already combined reducers.
// But writing it like just demonstrates how `combineReducers()`
// works.
const rootReducer = combineReducers({
apartmentStatus: combineReducers({
furniture: combineReducers({
damageLevel: damageLevelReducer,
orderStatus: furnitureOrderStatusReducer
}),
waterLevel: waterLevelReducer
}),
insuranceClaims: combinedReducer({
policies: policiesReducer,
damagedFurnitureClaims: furnitureClaimsReducer
}),
floodCount: floodCountReducer
})
If you then called store.getState()
you'd end up with an application state with the following keys:
{
apartmentStatus: {
furniture: {
damageLevel: ...,
orderStatus: ...
},
waterLevel: ...
},
insuranceClaims: {
policies: ...,
damagedFurnitureClaims: ...
},
floodCount: ...
}
Again, the example above helps illustrate that concept, but you probably wouldn't combine them all at once like this. Doing so muddles concerns and breaks encapsulation. More likely you'd combine the "sub-reducers" first, then import the reducers this:
// You would likely export *already combined reducers*
// this way you can encapsulate all related concerns.
// That way this code doesn't know or care whether the
// reducers it imports here are "simple" reducers or
// if they're the result of combining other reducers
import combinedApartmentStatusReducer from './apartment-reducer'
import insuranceClaimsReducer from './insurance-claims-reducer'
import floodCountReducer from './flood-count-reducer'
const rootReducer = combineReducers({
apartmentStatus,
insuranceClaims,
floodCount
})
Ok so how do I write these reducer thing-a-ma-bobbers anyway?
Well, the style you use inside the function is up to you as the developer. The critical thing is that you always return the relevant state based on the actions you are processing.
One thing is crucial to understand about reducers:
Every reducer will get called for every action and whatever it returns will be the new state tracked by that reducer
So even if your reducer doesn't do anything with a particular action, it still always has to return its unmodified state. Also, you have to return something even if you haven't yet received any action that is relevant to the reducer. Therefore, you cannot have a reducer that maintains a state of undefined
until an action comes along that changes that. Your reducer must return some initial state even before your app does anything. If you truly want a reducer to start out storing nothing, you can set starting state to null
.
Setting your initial state for a reducer is typically done using default function parameters as in the simple addition function below. Function params default to undefined
if not specified by the caller.
// by setting default to 0
function add(a = 0, b = 0) {
return a + b
}
// when we call it without arguments
// it prints out 0 instead of `NaN`
console.log(add()) // prints 0 not NaN
Similarly, for a reducer you'll want to provide initial state as a default parameter:
// when dealing with objects, as we often do
// I find it cleaner and more readable to define
// the initial state above the reducer as a variable
const initialState = {
loading: false,
data: null
}
// NOTE: we set `state = initialState` here.
export default (state = initialState, action) => {
// handle relevant action types here
return state
}
Redux will slap you on the wrist if you don't return anything by spitting out errors in your JS console. This brings us to the first rule that Redux imposes for reducers:
Redux reducer rule #1: You may never return undefined
, so you always have to return something, even if it's null
. Returning null
means your function at least intentionally returned something. This check is a bit of a safeguard to protect you from scenarios where you accidentally forget to return, and inadvertently return undefined
.
In the example, you may have noticed that initialState.data
is null
. Some developers shy away from using null
and instead would leave that value out. So instead of doing:
const initialState = {
loading: false,
data: null
}
We could have just left data
out entirely, right?:
const initialState = {
loading: false
}
While leaving it out works just fine, it isn't as explicit. I prefer using null
to indicate that there's nothing here now, but there will be. It serves as a hint to anyone reading the code, including yourself, that you intend to maintain a property called data
that will sometimes contain a value.
Ok, on to the second rule, which will require a bit more explanation.
Redux reducer rule #2: If you change it, replace it.
Another way to put this (if you want to impress your friends) is by saying that state is "immutable."
As it turns out, when we separate the application view/UI from the application state (as most frameworks to do to some extent), we inevitably end up in a position where we need to know if our state has changed, so our view can know whether it needs to update the DOM.
Objects in JavaScript are passed by reference. If you don't understand what I mean by this, study the next two examples carefully; this is a really vital thing to understand. If you already fully understand what I mean by this feel free to skip ahead to the "Understanding Immutability" section.
const one = {
isAwesome: true
}
// this is just creating another reference
// to the same object
const two = one
// so really, they're still the same object
console.log(two === one) // logs `true`
// even if we change the value of one
// we're changing both of them.
two.isAwesome = false
// so not only are these are still the same object
console.log(two === one) // still logs `true`
// we actually changed the value of both of them
console.log(two.isAwesome, one.isAwesome) // logs `false`, `false`
So, that means if we change values inside an object and we want to know if something has changed, we'd have to make a full copy so we can store individual properties, and then compare each property of the new and old object to determine what, if anything, has changed.
So we could do something like:
// start the same way
const one = {
isAwesome: true
}
// Now we could use `Object.assign()` to copy properties from
// `one` onto a new object `{}` and now we actually have two
// separate objects.
const two = Object.assign({}, one)
// now we could use a "deep" comparison function such as the
// `isEqual` method in the lodash library to see if the object
// properties are the same.
lodash.isEqual(one, two) // logs `true`
// but now if we change one
one.isAwesome = false
// and we do a "deep" comparison again
// their properties have now diverged
lodash.isEqual(one, two) // logs `false`
Understanding immutability
The approach of looping through an object to check for differences may work fine when you've only got a few things to keep track of, but in a large application you often have lots of properties you're tracking, on potentially thousands of objects. Checking each one of them any time any action occurs starts to become a big performance issue.
We can use the concept of "immutability" to help address this problem. As it turns out, checking whether something is the same reference is much faster/easier. So rather than performing a deep comparison of properties with a utility function like isEqual
from the Lodash library:
lodash.isEqual(object1, object2)
Instead, if we could somehow know that any time state inside an object changed that we'd get a new object reference, then our check could be simplified to just this:
object1 === object2
Unsurprisingly, that type of comparison is much much faster in JavaScript. Because instead of having to loop through and compare every value of every property in an object, we instead just check if we got the same object, or a different object!
That’s the basic idea of “immutability.” It’s doesn't necessarily mean that we make objects that are somehow frozen, so they cannot be changed. It means that we don't change the old objects, we replace them instead. I know I was confused by this initially. It is possible to implement enforced immutability with tools like Immutable.js, but you don’t need tools for it. Plain JavaScript will do fine, thank you.
To do this, we follow the immutability rule: “If you change it, replace it.” So what does that look like in code?
Rather than doing:
const obj = {
something: 'some value',
other: 'another property value'
}
// here we’re just editing `obj` in place
obj.something = 'some other value'
Instead, you do it like this:
const obj = {
something: 'some value',
other: 'the original value'
}
// Object.assign copies properties from all the objects
// onto the first object from left to right.
const newObject = Object.assign({}, obj, { something: 'some other value' })
// So now without changing `obj` we've created a brand new object
// that contains all previous properties and includes the new value
// for our changed `.something` property
// {
// something: 'some other value',
// other: 'the original value'
// }
// Using "Object spread"
// If your environment supports it, you can
// also use Object spread syntax to accomplish the
// same thing as Object.assign
const anotherNewObject = { ...obj, something: 'some other value' }
We can do the same with arrays of objects, rather than editing them in place:
const myStuff = [{ name: 'Henrik' }]
// push modifies the array defined above
myStuff.push({ name: 'js lovin fool' })
You can return a new array, which can we can do several different ways:
let myStuff = [
{ name: 'henrik' }
]
// Array.prototype.concat can be used to return a
// new array with a new item at the end:
myStuff = myStuff.concat([{ name: 'js lovin fool' }])
// or at the beginning:
myStuff = [{ name: 'js lovin fool' }].concat(myStuff)
// The same can done with the spread "..." operator
// if supported:
myStuff = [...myStuff, { name: 'js lovin fool' }]
// or:
myStuff = [{ name: 'js lovin fool' }, ...myStuff]
// .filter works great for removing items
myStuff = myStuff.filter((item => item.name === 'henrik'))
// we can also change items in place with `.map`
// but we have to be sure we create new objects for
// the items in the list we want to change:
myStuff = myStuff.map((item => {
// editing one item
if (item.name === 'henrik') {
return Object.assign({}, item, { isNerdy: true })
}
// return all the ones we're not changing
return item
})
// we can also use .map to replace items entirely
myStuff = myStuff.map(item => {
if (item.name === 'henrik') {
// a whole new object
return { name: 'someone else who is cooler' }
}
return item
})
So, this is what we must do in our reducers whenever we are updating state in Redux.
As it turns out, following this convention of immutable state enables other useful patterns, such as efficient "selectors" which we'll get into later.
Putting this all into a working, complete reducer:
Let's go back to our image search example and now actually write a reducer and create a store
to maintain our application state:
import { createStore } from 'redux'
// same starting state
// as the example in the previous chapter
const initialState = {
query: '',
fetching: false,
results: []
}
// a reducer that handles two different actions
const reducer = (state = initialState, action) => {
// when we get the news that a search has started
// we'll create and return a new object
// This object, will contain all the properties
// from current state, but.. will now also store
// the query from our `action.payload` and
// will set our `fetching` property to true
if (action.type === 'SEARCH_STARTED') {
return Object.assign({}, state, {
query: action.payload,
fetching: true
})
}
// when a search is complete, it will include results.
// So now, we'll store the results from action.payload
// and also make sure we set fetching back to `false`
if (action.type === 'SEARCH_COMPLETED') {
return Object.assign({}, state, {
results: action.payload,
fetching: false
})
}
// no matter what, we always return state
return state
}
// now we can take this and create a store
const store = createStore(reducer)
Ok, so how does the app know if the state was modified?
So far, we've covered two of the four methods on a Redux store: .getState()
and .dispatch()
. Being able to know when something has happened is made possible via the third method: .subscribe()
The subscribe method is a bit like registering an event listener with addEventListener()
, but simpler. The store.subscribe()
function takes a single argument: a callback function. The callback function will be called by Redux whenever an action has been dispatched. Redux doesn't attempt to tell you what or even if something has changed. In fact, Redux doesn't pass any arguments to the callback at all. So store.subscribe()
is simply a way to be notified that an action was dispatched, and therefore, something may have changed.
You may be surprised, but there is not a corresponding store.unsubscribe()
method. Instead, store.subscribe()
returns a function that you can call to cancel the subscription.
Updating our image search example
The simplest possible way to update a view is just store.subscribe()
and then in that function, call store.getState()
and re-render.
// assume that `store` here is the Redux store
// we created above and `view` knows how to
// update the DOM based on the application state
// returned by `store.getState()`
// To "bind" our view to our store state
// we could simply render our initial view
// once.
view(store.getState())
// Then subscribe to the store and run our view
// function again with the new state, any time
// something has changed.
store.subscribe(() => {
view(store.getState())
})
For our example we'll make the following changes:
- We'll create a store and pass it as a prop to our App component like this:
<App store={store}/>
- We'll update our
updateSearchQuery()
method to dispatch actions on the store. - We'll make sure our component is updated when the state changes by using a super simple binding technique. We'll define a
constructor
that grabs state from our store as starting state for the component and callsstore.subscribe()
to make sure future updates are reflected too:
With these minimal changes, our app now updates with any change to our store. Later on we'll cover the official way to update components when our application state changes, but for now, this will do the trick:
class App extends Component {
constructor(props) {
super(props)
const store = props.store
// grab our starting state
this.state = store.getState()
// subscribe to our store
store.subscribe(() => {
// set the result to component state
this.setState(store.getState())
})
}
updateSearchQuery(query) {
const store = this.props.store
store.dispatch({ type: 'IMAGE_SEARCH_STARTED', payload: query })
// start our image search
imageSearch(query).then(results => {
store.dispatch({ type: 'IMAGE_SEARCH_FINISHED', payload: results })
})
}
// everything else is unchanged
// ...
}
Runnable Example
A runnable, editable version of the image search application now using a simple Redux store is available at: https://reduxbook.com/image-search-redux
Different ways to write reducers
There are many ways to write the body of a reducer. Since it's common in Redux and other FLUX-like implementations to use switch
statements to inspect the action type, I'll demonstrate that as well:
// a reducer that handles two different actions
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'SEARCH_STARTED':
return Object.assign({}, state, {
query: action.payload,
fetching: true
})
case 'SEARCH_COMPLETED':
return Object.assign({}, state, {
results: action.payload,
fetching: false
})
default:
return state
}
}
This works fine also. Use whatever style suits you best. Personally, I find using switch
statements less legible and noisier. From what I've read there's no significant performance advantage to using a switch
over if
statements. If you had a massive amount of action types and really wanted to eek out every last drop of performance, lookup tables tend to be the fastest approach. You could create a lookup table of functions for your reducer like this:
const cases = {
SEARCH_STARTED: (state, action) =>
Object.assign({}, state, {
query: action.payload,
fetching: true
}),
SEARCH_COMPLETED: (state, action) =>
Object.assign({}, state, {
results: action.payload,
fetching: false
})
}
const reducer = (state = initialState, action) => {
const handler = cases[action.type]
return handler ? handler(state, action) : state
}
But again, I don't like this approach either because it's less legible. I prefer the simplicity and legibility of if (type === 'ACTION_TYPE')
where each condition returns the new state if it's matched. When we use this simple style, the code ends up reading in a way that describes exactly what I want it to do. It says if we're processing a certain action type, then return this updated state.
If you read my first book, you'll know that I consider readability of code to be very important. This book is called "Human Redux," after all. Let's optimize for humans! If you have a performance bottleneck in your app, this won't be it.
Congratulations!
You now understand the very basics of Redux.
Chapter recap
- In Redux "actions" are like news reports. They tell whoever may care that something has happened.
- Actions have to be "plain" JavaScript objects that have a
type
property. - Actions often have a payload of some type. I recommend always attaching additional details about the action as a property called
payload
. - The state is updated and managed by reducers.
- Reducers have the signature:
(state = initialState, action) => newState
- Reducers always have to return something even if it's
null
; they should never returnundefined
. - If a reducer's state is an object, you must always return a new object instead of editing the object in place.
- One easy way to create new objects when you need to update state is to use the pattern:
return Object.assign({}, state, {THINGS YOU WANT TO CHANGE})
createStore()
requires a single argument, which is the "root reducer" and returns astore
.- The
store
only has four methods total:store.getState()
: Returns the plain object of all the store's statestore.dispatch(action)
: Sends an action through all the reducersstore.subscribe(fn)
: Takes a callback function to be called each time something has changed in the state. This method returns a function that can be called to unsubscribe.store.replaceReducer()
: We won't cover this because most people will never use it. But it lets you replace the root reducer of an already created store.
- The updated image search example, now using Redux is available here: https://reduxbook.com/image-search-redux