Overview
Recently, I put together a short 3-hour tutorial on using React
with the Redux state management API. I chose to write a simple D&D style game
using simple icons and a backing sound track, complete with (very
rudimentary) non-player characters. All of this was in service of making
the seminar fun, and giving the students something to hack on.
After the talk, I got to talking with one of the attendees, and our
discussions made the codebase even better (it’s great when you can learn
from your attendees and make what you built even better), and I spent a
few days really making the project more simple and dirct.
This post is the beginning of a series that delves into the Redux world,
using that codebase to illustrate some techniques and challenges. Here,
we’ll start first with a very simple introduction into React state
management, then discuss how Redux can help streamline state management
for your application.
Later posts will delve even deeper into Redux. I hope that we come out
the other side with a good set of techniques and helpful signposts to
aid your learning along the way.
The repository, if you’re interested in looking ahead, is at
github.com/krimple/fooberry.
React and State Management
React projects break data up into two categories:
state
: read-write data that lives within a componentprops
: read-only data sent to a component, may be updated later by
the sending component (a parent)
Anyone who has spent significant time in a React application realizes
that, as the component graph gets bigger, the connections between the
deeper components in the tree and their parents can become a chore to
manage.
Component connections like this become common:
passing props, the hard way through other components.
What about a design that involves containers for each task for layout
purposes?
passing props, the hard way through other components.
Of course, using something like { this.props.children }
within the
container to embed the child can help reduce the over-passing of
properties in decorative components like cards and panels:
// in TaskList.js render() { const cards = this.props.tasks.map(t => { return ( <Card> <Task key={t.id} task={t} /> <1> </Card> ); } // in Card.js render() { <div className="card"> { this.props.children } <2> </div> }
- A nested component in your template
- Emits all nested child components (in our case, the Task)
Event Propagation: a cross-cutting challenge
Consider a functional component at the bottom of a component graph, one
that needs to signal a logout
event or message to the application.
Typically the component that needs to receive the message / event will
pass one of its functions to handle the event downward to the child
component that needs it:
// in top component handleLogout => () { // log out! } render() { ... <MainPanel onLogout={ this.handleLogout } /> ... } // MainPanel render() { <ButtonBar onLogout={ this.props.onLogout } /> } // ButtonBar <Button onClick={ this.props.onLogout }>Logout</Button>
These types of events may require cleanup in more than just a single
parent component. Perhaps the accessible features delivered in a menu
need to change, or the main view needs to clean up and remove any
secured data until the next time the user logs in.
These concerns, then, are cross-cutting. They affect more than one
component, and if determined to be global, will have to be factored into
a bi-directional data and event binding model that can become seriously
complex very quickly.
When you end up sending props from parent to child in a deeper hierarchy
or sending events from child to parents, it becomes difficult to
refactor your code. Any intermediate component that merely passes the
prop needs to lose it if the prop is sent down another path, and it
becomes difficult to quickly move a component from one tree to another.
Applications with prop-passing also attempt to limit the amount of data
in any one level by storing the state at the component where it makes
the most topological sense, not just at the root.
But that strategy breaks down for the cross-cutting concerns.
Tip
In storing lots of small state data in a smattering of places, you
lose control over the state in the large, and you have trouble sharing
that state content to siblings or other non-tree levels.
When you’re considering building an application with dozens or hundreds
of individual components and use cases, you need a way to manage state
in a more organized way. Enter Redux.
Redux – state management for Javascript
Redux is a simple state management engine for Javascript. It is not tied
directly to React or any other view technology; however, because it is
single-purpose and extensible, it is a perfect companion for React
projects.
Note
Some AngularJS, Angular and Ember developers also use Redux or a
similar project as a way to manage shared application state.
Redux has a simple API, composed of a State Store, Actions, Action
Creators, and Reducers:
- An
action
is an event processed by Redux. It has a type, and a
payload of request data. - An 'action creator` is a function that creates the data for an
action. It assembles the action with atype
and with the
appropriate payload so that the caller of the API doesn’t need to
know the internal action structure. - A
reducer
is a function that takes two parameters, the existing
state and an action request. The reducer function uses the action
type and payload to determine what work to perform. Ultimately the
reducer either returns the existing state unchanged, or a new state
based on activities requested for that action type. - Actions are created by action creators and dispatched by the
state store.
Most importantly, this activity happens synchronously.
Data flow in Redux.
Click to enlarge…
To translate in words instead: an Action Creator creates an Action
object, which is then sent to the Redux Store’s dispatch
function to
be processed by the Reducer(s) installed in the system. Optional
middleware sits between the dispatching process and the reducer and
can modify the behavior before or after (or in replacement of) the
dispatch of the request to the store.
Tip
Middleware APIs deal with incoming requests or outgoing results, and
in aspect-oriented programming terms provide a sort of around
advice. Some common middlewares: Redux
Logger, Redux
Thunk, and Redux
Saga are common middleware
APIs.
An example State Store
Let’s review how we could use Redux to manage the state of a simple
mathematical calculator. Although a trivial example, it shows all the
working pieces needed.
Note: we’re going to use the Ducks Design
Pattern to organize our
reducer. This pattern places the actions, action creators and reducer in
a single file for clarity. We’ll also use ES2015+ syntax so you can see
how the latest spread operators make managing the state quite easy.
We’ll start with the actions:
In src/redux/calcReducer.js
:
const ADD = 'calculator/redux/calc/ADD'; <1> const SUBTRACT = 'calculator/redux/calc/SUBTRACT'; <1>
- Actions should contain the value of the app or module, followed by
the term 'redux', followed by the reducer subject area, followed by
the uppercase constant of the action variable.
Next we have the actual reducer function. This function is run every
time a caller dispatches an action request:
function reducer(state = 0, action) { <1> switch(action.type) { case ADD: <2> return action.a + action.b; case SUBTRACT: <2> return action.a - action.b; default: <3> return state; }
- Reducers are functions that receive the current state and action
request and return a replacement state or the same state - Each of our actions keys off of an action
type
– this property is
required in all actions - Each reducer should have a default response which returns the
existing state. The Redux engine sends various probe operations to
the state store, and also if you end up setting up multiple reducers
to each manage a portion of a store, your reducer may receive
requests it doesn’t need to act on
Reducers must return a brand new state reference each time they change
something. In the case above, we’re only using a single value, so we
replace it. But if we wanted to hold a calculation history, we could do
that too:
Reducers take an action and existing state, and make a new state (or
not).
function reducer(state = { history: [], answer: 0 }, action) { switch(action.type) { case ADD: let answer = action.a + action.b; let logEntry = action.a + ' + ' + action.b + ' = ' answer; return { answer: action.a + action.b, history: [ ...history, logEntry ] }; case SUBTRACT: let answer = action.a + action.b; let logEntry = action.a + ' + ' + action.b + ' = ' answer; return { answer: action.a + action.b, history: [ ...history, logEntry ] }; default: return state; }
This could be simplified further. The main thing to focus on is the fact
that each time, we return a new state object. If the operation wasn’t
one we care about, we ignore it and return the existing state.
Finally, the action creators:
Action creators simply create the payload for an action. They can
validate parameters and serve as pre-storage business logic functions
too.
export function add(a, b) { return { type: ADD, a: a, b: b }; } export function subtract(a, b) { return { type: SUBTRACT, a: a, b: b }; }
These action creators are, as you can see, simple functions that return
the appropriate payload.
Testing A Reducer
Since our reducer is a simple Javascript function, we can use any
testing engine to test it. A simple test using Jasmine might look like:
Testing a Reducer with Jasmine.
import reducer, { add, subtract } from './calcReducer'; describe('Calculator Spec', () => { it('should add', () => { const newState = reducer(0, add(5, 2)); <1> expect(newState).toBe(7); <2> }); it('should subtract', () => { const newState = reducer(0, subtract(5, 3)); <1> expect(newState).toBe(2); <2> }); });
- Reducers are pure functions. They have no side effects. Therefore
you can call them directly. We import the reducer directly here in
our test. - Call the reducer with the current state (assume 0 for the current
calculator answer) and the action. This will return the new state. - Verify that the new state values are correct for the given inputs.
Reducers are simple. Action Creators can be Complex.
That’s all there is to it. Reducers simply boil down dispatches of
action requests to new states.
Most of the complexity of a Redux store is in fetching or validating the
state change requests. This should not be done within the reducers,
which only update the data of the store, but in the action creators.
Action creators are also where asynchronous work happens before or
after updating the store.
Subscribe to a store to get updated state
There are two query APIs – getState()
and subscribe()
. getState
is
meant to be used when you imperatively have to know, right now, what the
data is in the store. subscribe
lets you be notified whenever the
state changes.
The key to decoupling your React component tree from the data itself is
to strategically dispatch action requests and bind subscriptions to
pieces of the tree where needed, letting Redux own the data and the
components own the user interface.
You could integrate Redux with your components by hand, as in something
like this:
Integrating Redux by hand. Don’t do this.
import store from './my-state-store'; export class LogDisplayer extends Component { constructor(props) { store.subscribe((tree) => { this.setState({ logMessages: tree.logmessages }); }); } ... }
The problem: now you have state both in the reducer, and a shadow copy of the state
in the component. As long as you are strict about not modifying the state inside the
component, you could live with this, but it’s not ideal.
You could subscribe to the store in a parent component, feeding the data
back into a child Task Manager via props, but isn’t that seeming rather
familiar?
You could even get funky and use the Context
object, which lets you
share data context between components, but that’s supposed to be a
non-final API and subject to change. (Actually, this IS the API that
some of our integration libraries use, but you never touch it directly).
What’s a budding Reactive Reduxian to do?
Enter React-Redux, a Redux integration library
The react-redux library
is dubbed the official bindings API for Redux for the React API. It
consists of three APIs:
- The
<Provider />
container component, which wrapsRedux
into
your component tree and makes the next feature available to all
child components connect
– A function with a terrible name (should be
connectComponentToRedux
or something like that) that wraps your
component with a subscription to Redux, and updates read-only
props
in your component when anything changes. It also adds a prop
nameddispatch
that can be used to dispatch requests to the state
storemapStateToProps
– a function that you can call fromconnect
to
perform the mapping from redux state to your react props.mapDispatchToProps
– a function that can wire action creators to
the dispatch method automatically, making them appear as normal
member methods on theprops
object. Note, this is only
really used when you want to pass a function to a sub-component but don’t want it to know
anything about Redux. We show the syntax, but since thedispatch
method is
passed to the component when it is connected, don’t bother with this mapping function
when connecting single component to Redux.
A Reactive Reduxian Calculator
Now our calculator can use the simplest form of integration with the
react-redux
library, and bind data from the store to the properties it
needs to display and edit.
The top-level component for our Calculator app, wired to Redux with
React-Redux.
// Our App.js starting component import React from 'react'; import { Provide } from 'react-redux'; <1> import store from './my-state-store'; import Calculator from './src/Calculator.js'; export default const App => () { return( <Provider store={store}> <2> <Calculator /> </Provider> ) }
- The
react-redux
library provides this wrapper component, which
stores the state in the ReactContext
object. - We surround our outermost component with the
Provider
component,
passing it our created Redux store. Now any component inside of this
hierarchy can access the store. - Our
Calculator
component now has access to the Redux store.
The Calculator itself, wired now with Redux via the React-Redux
integration library.
// The Calculator.js component import React, { Component } from 'react'; <1> import { connect } from 'react-redux'; import { add, subtract } from './calcReducer'; class Calculator extends Component { render() { return ( <input value={ this.state.a } onChange={ this.setA }><br/> <2> <input value={ this.state.b } onChange={ this.setB }><br/> <2> <div> Result { this.state.answer } </div> <h3>Actions</h3> <div> <button onClick={ this.add }> + </button> <3> <button onClick={ this.subtract }> - </button> <3> </div> ); } // handle events this.setA = (event) => { this.state.a = event.target.value; }; <4> this.setB = (event) => { this.state.b = event.target.value; }; <4> this.add = () => { <5> this.dispatch(add(this.state.a, this.state.b)); }; this.subtract = () => { <5> this.dispatch(subtract(this.state.a, this.state.b)); }; } // here's where we map the state from Redux to our props function mapStateToProps(state) { <6> return { a: state.a || 0, b: state.b || 0, answer: state.answer || 0 }; } // the way to connect a component to redux is // to return its connected proxy export default connect(mapStateToProps)(Calculator); <7>
- Components within the
Provider
will now use theconnect
function
to subsribe themselves into the Redux store. - Expose controlled inputs which are fed by the Redux state received
by the component - Expose method calls that dispatch requests to Redux to add or
subtract - Provide methods to ingest changes to bound state for the operands
- Execute the
dispatch
method (provided by theconnect
function
below, which dispatches an action request to Redux - Provide a Redux State to React Props mapping function. This
is executed every time a state change is made in redux, so it
should be a very simple and quick function. In our case, we just
receive the updated state fora
,b
, andanswer
and bind them
to props with the same name. Thedispatch
function is bound to
props
for free with this function. - Finally, we call
connect
, which takes our Redux state mapping
function, and returns a function that can wrap our React class
with the properRedux
proxy. This is then called with our class to
provide a Redux-connected React component.
Tip
mapStateToProps
is poorly named. So isconnect
. Whoever came up
with these too-generic names should probably fix that someday.
Remember it’s map redux state to react props, and connect react
component to redux store. If it helps you, write the mapping
function asmapReduxStateToReactProps
. Sinceconnect
is a named
import, you’d have to go out of your way to rename it, so we suggest
you don’t.
Wrap-up
This sample is a trivial one, so don’t infer that you want to send all
of your Redux state data into a single component, manage all application
state in Redux, or that there isn’t more to the mapping logic. In the
next few blog posts, we’ll explore how to improve the request
dispatching process, manage asynchronous updates, how to manage complex
state effectively, and how to test your reducers.
The Plug – React and Redux Training
Since you've read this far, and hopefully you found the article useful, consider
attending our React and Friends training course.
Offered onsite at your location, for your employees, we take you from the basics
of building a React application through calling RESTful APIs, dealing with forms,
handling state in props/state and Redux, and learning how to use the React Router.