Movatterモバイル変換


[0]ホーム

URL:


Skip to main content

Redux Fundamentals, Part 4: Store

What You'll Learn
  • How to create a Redux store
  • How to use the store to update state and listen for updates
  • How to configure the store to extend its capabilities
  • How to set up the Redux DevTools Extension to debug your app

Introduction

InPart 3: State, Actions, and Reducers, we started writing our example todo app. Welisted business requirements, defined thestate structure we need to make the app work, and created a series of action typesto describe "what happened" and match the kinds of events that can happen as a user interacts with our app. We also wrotereducer functions that can handle updating ourstate.todos andstate.filters sections, and saw how we can use the ReduxcombineReducers functionto create a "root reducer" based on the different "slice reducers" for each feature in our app.

Now, it's time to pull those pieces together, with the central piece of a Redux app: thestore.

caution

Note thatthis tutorial intentionally shows older-style Redux logic patterns that require more code than the "modern Redux" patterns with Redux Toolkit we teach as the right approach for building apps with Redux today, in order to explain the principles and concepts behind Redux. It'snot meant to be a production-ready project.

See these pages to learn how to use "modern Redux" with Redux Toolkit:

Redux Store

The Reduxstore brings together the state, actions, and reducers that make up your app. The store has several responsibilities:

It's important to note thatyou'll only have a single store in a Redux application. When you want to split your data handling logic, you'll usereducer composition and create multiple reducers thatcan be combined together, instead of creating separate stores.

Creating a Store

Every Redux store has a single root reducer function. In the previous section, wecreated a root reducer function usingcombineReducers. That root reducer is currently defined insrc/reducer.js in our example app. Let's import that root reducer and create our first store.

The Redux core library hasacreateStore API that will create the store. Add a new filecalledstore.js, and importcreateStore and the root reducer. Then, callcreateStore and pass in the root reducer:

src/store.js
import{ createStore}from'redux'
importrootReducerfrom'./reducer'

const store=createStore(rootReducer)

exportdefault store

Loading Initial State

createStore can also accept apreloadedState value as its second argument. You could use this to addinitial data when the store is created, such as values that were included in an HTML page sent from the server, or persisted inlocalStorage and read back when the user visits the page again, like this:

storeStatePersistenceExample.js
import{ createStore}from'redux'
importrootReducerfrom'./reducer'

let preloadedState
const persistedTodosString=localStorage.getItem('todos')

if(persistedTodosString){
preloadedState={
todos:JSON.parse(persistedTodosString)
}
}

const store=createStore(rootReducer, preloadedState)

Dispatching Actions

Now that we have created a store, let's verify our program works! Even without any UI, we can already test the update logic.

tip

Before you run this code, try going back tosrc/features/todos/todosSlice.js, and remove all the example todo objects from theinitialState so that it's an empty array. That will make the output from this example a bit easier to read.

src/index.js
// Omit existing React imports

importstorefrom'./store'

// Log the initial state
console.log('Initial state: ', store.getState())
// {todos: [....], filters: {status, colors}}

// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe= store.subscribe(()=>
console.log('State after dispatch: ', store.getState())
)

// Now, dispatch some actions

store.dispatch({type:'todos/todoAdded',payload:'Learn about actions'})
store.dispatch({type:'todos/todoAdded',payload:'Learn about reducers'})
store.dispatch({type:'todos/todoAdded',payload:'Learn about stores'})

store.dispatch({type:'todos/todoToggled',payload:0})
store.dispatch({type:'todos/todoToggled',payload:1})

store.dispatch({type:'filters/statusFilterChanged',payload:'Active'})

store.dispatch({
type:'filters/colorFilterChanged',
payload:{color:'red',changeType:'added'}
})

// Stop listening to state updates
unsubscribe()

// Dispatch one more action to see what happens

store.dispatch({type:'todos/todoAdded',payload:'Try creating a store'})

// Omit existing React rendering logic

Remember, every time we callstore.dispatch(action):

  • The store callsrootReducer(state, action)
    • That root reducer may call other slice reducers inside of itself, liketodosReducer(state.todos, action)
  • The store saves thenew state value inside
  • The store calls all the listener subscription callbacks
  • If a listener has access to thestore, it can now callstore.getState() to read the latest state value

If we look at the console log output from that example, you can see how theRedux state changes as each action was dispatched:

Logged Redux state after dispatching actions

Notice that our app didnot log anything from the last action. That's because we removed the listener callback when we calledunsubscribe(), so nothing else ran after the action was dispatched.

We specified the behavior of our app before we even started writing the UI. Thathelps give us confidence that the app will work as intended.

info

If you want, you can now try writing tests for your reducers. Because they'repure functions, it should be straightforward to test them. Call them with an examplestate andaction,take the result, and check to see if it matches what you expect:

todosSlice.spec.js
importtodosReducerfrom'./todosSlice'

test('Toggles a todo based on id',()=>{
const initialState=[{id:0,text:'Test text',completed:false}]

const action={type:'todos/todoToggled',payload:0}
const result=todosReducer(initialState, action)
expect(result[0].completed).toBe(true)
})

Inside a Redux Store

It might be helpful to take a peek inside a Redux store to see how it works. Here's a miniature example of a working Redux store, in about 25 lines of code:

miniReduxStoreExample.js
functioncreateStore(reducer, preloadedState){
let state= preloadedState
const listeners=[]

functiongetState(){
return state
}

functionsubscribe(listener){
listeners.push(listener)
returnfunctionunsubscribe(){
const index= listeners.indexOf(listener)
listeners.splice(index,1)
}
}

functiondispatch(action){
state=reducer(state, action)
listeners.forEach(listener=>listener())
}

dispatch({type:'@@redux/INIT'})

return{ dispatch, subscribe, getState}
}

This small version of a Redux store works well enough that you could use it to replace the actual ReduxcreateStore function you've been using in your app so far. (Try it and see for yourself!)The actual Redux store implementation is longer and a bit more complicated, but most of that is comments, warning messages, and handling some edge cases.

As you can see, the actual logic here is fairly short:

  • The store has the currentstate value andreducer function inside of itself
  • getState returns the current state value
  • subscribe keeps an array of listener callbacks and returns a function to remove the new callback
  • dispatch calls the reducer, saves the state, and runs the listeners
  • The store dispatches one action on startup to initialize the reducers with their state
  • The store API is an object with{dispatch, subscribe, getState} inside

To emphasize one of those in particular: notice thatgetState just returns whatever the currentstate value is. That means thatby default, nothing prevents you from accidentally mutating the current state value! This code will run without any errors, but it's incorrect:

const state= store.getState()
// ❌ Don't do this - it mutates the current state!
state.filters.status='Active'

In other words:

  • The Redux store doesn't make an extra copy of thestate value when you callgetState(). It's exactly the same reference that was returned from the root reducer function
  • The Redux store doesn't do anything else to prevent accidental mutations. Itis possible to mutate the state, either inside a reducer or outside the store, and you must always be careful to avoid mutations.

One common cause of accidental mutations is sorting arrays.Callingarray.sort() actually mutates the existing array. If we calledconst sortedTodos = state.todos.sort(), we'd end up mutating the real store state unintentionally.

tip

InPart 8: Modern Redux, we'll see how Redux Toolkit helps avoid mutations in reducers, and detects and warns about accidental mutations outside of reducers.

Configuring the Store

We've already seen that we can passrootReducer andpreloadedState arguments tocreateStore. However,createStore can also take one more argument, which is used to customize the store's abilities and give it new powers.

Redux stores are customized using something called astore enhancer. A store enhancer is like a special version ofcreateStore that adds another layer wrapping around the original Redux store. An enhanced store can then change how the store behaves, by supplying its own versions of the store'sdispatch,getState, andsubscribe functions instead of the originals.

For this tutorial, we won't go into details about how store enhancers actually work - we'll focus on how to use them.

Creating a Store with Enhancers

Our project has two small example store enhancers available, in thesrc/exampleAddons/enhancers.js file:

  • sayHiOnDispatch: an enhancer that always logs'Hi'! to the console every time an action is dispatched
  • includeMeaningOfLife: an enhancer that always adds the fieldmeaningOfLife: 42 to the value returned fromgetState()

Let's start by usingsayHiOnDispatch. First, we'll import it, and pass it tocreateStore:

src/store.js
import{ createStore}from'redux'
importrootReducerfrom'./reducer'
import{ sayHiOnDispatch}from'./exampleAddons/enhancers'

const store=createStore(rootReducer,undefined, sayHiOnDispatch)

exportdefault store

We don't have apreloadedState value here, so we'll passundefined as the second argument instead.

Next, let's try dispatching an action:

src/index.js
importstorefrom'./store'

console.log('Dispatching action')
store.dispatch({type:'todos/todoAdded',payload:'Learn about actions'})
console.log('Dispatch complete')

Now look at the console. You should see'Hi!' logged there, in between the other two log statements:

sayHi store enhancer logging

ThesayHiOnDispatch enhancer wrapped the originalstore.dispatch function with its own specialized version ofdispatch. When we calledstore.dispatch(), we were actually calling the wrapper function fromsayHiOnDispatch, which called the original and then printed 'Hi'.

Now, let's try adding a second enhancer. We can importincludeMeaningOfLife from that same file... but we have a problem.createStore only accepts one enhancer as its third argument! How can we passtwo enhancers at the same time?

What we really need is some way to merge both thesayHiOnDispatch enhancer and theincludeMeaningOfLife enhancer into a single combined enhancer, and then pass that instead.

Fortunately,the Redux core includesacompose function that can be used to merge multiple enhancers together. Let's use that here:

src/store.js
import{ createStore, compose}from'redux'
importrootReducerfrom'./reducer'
import{
sayHiOnDispatch,
includeMeaningOfLife
}from'./exampleAddons/enhancers'

const composedEnhancer=compose(sayHiOnDispatch, includeMeaningOfLife)

const store=createStore(rootReducer,undefined, composedEnhancer)

exportdefault store

Now we can see what happens if we use the store:

src/index.js
importstorefrom'./store'

store.dispatch({type:'todos/todoAdded',payload:'Learn about actions'})
// log: 'Hi!'

console.log('State after dispatch: ', store.getState())
// log: {todos: [...], filters: {status, colors}, meaningOfLife: 42}

And the logged output looks like this:

meaningOfLife store enhancer logging

So, we can see that both enhancers are modifying the behavior of the store at the same time.sayHiOnDispatch has changed howdispatch works, andincludeMeaningOfLife has changed howgetState works.

Store enhancers are a very powerful way to modify the store, and almost all Redux apps will include at least one enhancer when setting up the store.

tip

If you don't have anypreloadedState to pass in, you can pass theenhancer as the second argument instead:

const store=createStore(rootReducer, storeEnhancer)

Middleware

Enhancers are powerful because they can override or replace any of the store's methods:dispatch,getState, andsubscribe.

But, much of the time, we only need to customize howdispatch behaves. It would be nice if there was a way to add some customized behavior whendispatch runs.

Redux uses a special kind of addon calledmiddleware to let us customize thedispatch function.

If you've ever used a library like Express or Koa, you might already be familiar with the idea of adding middleware to customize behavior. In these frameworks, middleware is some code you can put between the framework receiving a request, and the framework generating a response. For example, Express or Koa middleware may add CORS headers, logging, compression, and more. The best feature of middleware is that it's composable in a chain. You can use multiple independent third-party middleware in a single project.

Redux middleware solves different problems than Express or Koa middleware, but in a conceptually similar way.Redux middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more.

First, we'll look at how to add middleware to the store, then we'll show how you can write your own.

Using Middleware

We already saw that you can customize a Redux store using store enhancers. Redux middleware are actually implemented on top of a very special store enhancer that comes built in with Redux, calledapplyMiddleware.

Since we already know how to add enhancers to our store, we should be able to do that now. We'll start withapplyMiddleware by itself, and we'll add three example middleware that have been included in this project.

src/store.js
import{ createStore, applyMiddleware}from'redux'
importrootReducerfrom'./reducer'
import{ print1, print2, print3}from'./exampleAddons/middleware'

const middlewareEnhancer=applyMiddleware(print1, print2, print3)

// Pass enhancer as the second arg, since there's no preloadedState
const store=createStore(rootReducer, middlewareEnhancer)

exportdefault store

As their names say, each of these middleware will print a number when an action is dispatched.

What happens if we dispatch now?

src/index.js
importstorefrom'./store'

store.dispatch({type:'todos/todoAdded',payload:'Learn about actions'})
// log: '1'
// log: '2'
// log: '3'

And we can see the output in the console:

print middleware logging

So how does that work?

Middleware form a pipeline around the store'sdispatch method. When we callstore.dispatch(action), we'reactually calling the first middleware in the pipeline. That middleware can then do anything it wants when it sees the action. Typically, a middleware will check to see if the action is a specific type that it cares about, much like a reducer would. If it's the right type, the middleware might run some custom logic. Otherwise, it passes the action to the next middleware in the pipeline.

Unlike a reducer,middleware can have side effects inside, including timeouts and other async logic.

In this case, the action is passed through:

  1. Theprint1 middleware (which we see asstore.dispatch)
  2. Theprint2 middleware
  3. Theprint3 middleware
  4. The originalstore.dispatch
  5. The root reducer insidestore

And since these are all function calls, they allreturn from that call stack. So, theprint1 middleware is the first to run, and the last to finish.

Writing Custom Middleware

We can also write our own middleware. You might not need to do this all the time, but custom middleware are a great way to add specific behaviors to a Redux application.

Redux middleware are written as a series of three nested functions. Let's see what that pattern looks like. We'll start by trying to write this middleware using thefunction keyword, so that it's more clear what's happening:

// Middleware written as ES5 functions

// Outer function:
functionexampleMiddleware(storeAPI){
returnfunctionwrapDispatch(next){
returnfunctionhandleAction(action){
// Do anything here: pass the action onwards with next(action),
// or restart the pipeline with storeAPI.dispatch(action)
// Can also use storeAPI.getState() here

returnnext(action)
}
}
}

Let's break down what these three functions do and what their arguments are.

  • exampleMiddleware: The outer function is actually the "middleware" itself. It will be called byapplyMiddleware, and receives astoreAPI object containing the store's{dispatch, getState} functions. These are the samedispatch andgetState functions that are actually part of the store. If you call thisdispatch function, it will send the action to thestart of the middleware pipeline. This is only called once.
  • wrapDispatch: The middle function receives a function callednext as its argument. This function is actually thenext middleware in the pipeline. If this middleware is the last one in the sequence, thennext is actually the originalstore.dispatch function instead. Callingnext(action) passes the action to thenext middleware in the pipeline. This is also only called once
  • handleAction: Finally, the inner function receives the currentaction as its argument, and will be calledevery time an action is dispatched.
tip

You can give these middleware functions any names you want, but it can help to use these names to remember what each one does:

  • Outer:someCustomMiddleware (or whatever your middleware is called)
  • Middle:wrapDispatch
  • Inner:handleAction

Because these are normal functions, we can also write them using ES2015 arrow functions. This lets us write them shorter because arrow functions don't have to have areturn statement, but it can also be a bit harder to read if you're not yet familiar with arrow functions and implicit returns.

Here's the same example as above, using arrow functions:

constanotherExampleMiddleware=storeAPI=>next=>action=>{
// Do something in here, when each action is dispatched

returnnext(action)
}

We're still nesting those three functions together, and returning each function, but the implicit returns make this shorter.

Your First Custom Middleware

Let's say we want to add some logging to our application. We'd like to see the contents of each action in the console when it's dispatched, and we'd like to see what the state is after the action has been handled by the reducers.

info

These example middleware aren't specifically part of the actual todo app, but you can try adding them to your project to see what happens when you use them.

We can write a small middleware that will log that information to the console for us:

constloggerMiddleware=storeAPI=>next=>action=>{
console.log('dispatching', action)
let result=next(action)
console.log('next state', storeAPI.getState())
return result
}

Whenever an action is dispatched:

  • The first part of thehandleAction function runs, and we print'dispatching'
  • We pass the action to thenext section, which may be another middleware or the realstore.dispatch
  • Eventually the reducers run and the state is updated, and thenext function returns
  • We can now callstoreAPI.getState() and see what the new state is
  • We finish by returning whateverresult value came from thenext middleware

Any middleware can return any value, and the return value from the first middleware in the pipeline is actually returned when you callstore.dispatch(). For example:

constalwaysReturnHelloMiddleware=storeAPI=>next=>action=>{
const originalResult=next(action)
// Ignore the original result, return something else
return'Hello!'
}

const middlewareEnhancer=applyMiddleware(alwaysReturnHelloMiddleware)
const store=createStore(rootReducer, middlewareEnhancer)

const dispatchResult= store.dispatch({type:'some/action'})
console.log(dispatchResult)
// log: 'Hello!'

Let's try one more example. Middleware often look for a specific action, and then do something when that action is dispatched. Middleware also have the ability to run async logic inside. We can write a middleware that prints something on a delay when it sees a certain action:

constdelayedMessageMiddleware=storeAPI=>next=>action=>{
if(action.type==='todos/todoAdded'){
setTimeout(()=>{
console.log('Added a new todo: ', action.payload)
},1000)
}

returnnext(action)
}

This middleware will look for "todo added" actions. Every time it sees one, it sets a 1-second timer, and then prints the action's payload to the console.

Middleware Use Cases

So, what can we do with middleware? Lots of things!

A middleware can do anything it wants when it sees a dispatched action:

  • Log something to the console
  • Set timeouts
  • Make asynchronous API calls
  • Modify the action
  • Pause the action or even stop it entirely

and anything else you can think of.

In particular,middleware areintended to contain logic with side effects. In addition,middleware can modifydispatch to accept things that arenot plain action objects. We'll talk more about both of thesein Part 6: Async Logic.

Redux DevTools

Finally, there's one more very important thing to cover with configuring the store.

Redux was specifically designed to make it easier to understand when, where, why, and how your state has changed over time. As part of that, Redux was built to enable the use of theRedux DevTools - an addon that shows you a history of what actions were dispatched, what those actions contained, and how the state changed after each dispatched action.

The Redux DevTools UI is available as a browser extension forChrome andFirefox. If you haven't already added that to your browser, go ahead and do that now.

Once that's installed, open up the browser's DevTools window. You should now see a new "Redux" tab there. It doesn't do anything, yet - we've got to set it up to talk to a Redux store first.

Adding the DevTools to the Store

Once the extension is installed, we need to configure the store so that the DevTools can see what's happening inside. The DevTools require a specific store enhancer to be added to make that possible.

TheRedux DevTools Extension docs have some instructions on how to set up the store, but the steps listed are a bit complicated. However, there's an NPM package calledredux-devtools-extension that takes care of the complicated part. That package exports a specializedcomposeWithDevTools function that we can use instead of the original Reduxcompose function.

Here's how that looks:

src/store.js
import{ createStore, applyMiddleware}from'redux'
import{ composeWithDevTools}from'redux-devtools-extension'
importrootReducerfrom'./reducer'
import{ print1, print2, print3}from'./exampleAddons/middleware'

const composedEnhancer=composeWithDevTools(
// EXAMPLE: Add whatever middleware you actually want to use here
applyMiddleware(print1, print2, print3)
// other store enhancers if any
)

const store=createStore(rootReducer, composedEnhancer)
exportdefault store

Make sure thatindex.js is still dispatching an action after importing the store. Now, open up the Redux DevTools tab in the browser's DevTools window. You should see something that looks like this:

Redux DevTools Extension: action tab

There's a list of dispatched actions on the left. If we click one of them, the right panel shows several tabs:

  • The contents of that action object
  • The entire Redux state as it looked after the reducer ran
  • The diff between the previous state and this state
  • If enabled, the function stack trace leading back to the line of code that calledstore.dispatch() in the first place

Here's what the "State" and "Diff" tabs look like after we dispatched that "add todo" action:

Redux DevTools Extension: state tab

Redux DevTools Extension: diff tab

These are very powerful tools that can help us debug our apps and understand exactly what's happening inside.

What You've Learned

As you've seen, the store is the central piece of every Redux application. Stores contain state and handle actions by running reducers, and can be customized to add additional behaviors.

Let's see how our example app looks now:

And as a reminder, here's what we covered in this section:

Summary
  • Redux apps always have a single store
    • Stores are created with the ReduxcreateStore API
    • Every store has a single root reducer function
  • Stores have three main methods
    • getState returns the current state
    • dispatch sends an action to the reducer to update the state
    • subscribe takes a listener callback that runs each time an action is dispatched
  • Store enhancers let us customize the store when it's created
    • Enhancers wrap the store and can override its methods
    • createStore accepts one enhancer as an argument
    • Multiple enhancers can be merged together using thecompose API
  • Middleware are the main way to customize the store
    • Middleware are added using theapplyMiddleware enhancer
    • Middleware are written as three nested functions inside each other
    • Middleware run each time an action is dispatched
    • Middleware can have side effects inside
  • The Redux DevTools let you see what's changed in your app over time
    • The DevTools Extension can be installed in your browser
    • The store needs the DevTools enhancer added, usingcomposeWithDevTools
    • The DevTools show dispatched actions and changes in state over time

What's Next?

We now have a working Redux store that can run our reducers and update the state when we dispatch actions.

However, every app needs a user interface to display the data and let the user do something useful. InPart 5: UI and React, we'll see how the Redux store works with a UI, and specifically see how Redux can work together with React.


[8]ページ先頭

©2009-2025 Movatter.jp