Building a React Application: Part II
In the first part of this tutorial, we built a basic React application with Create React App and covered some core concepts like components, hooks, and routing. With that knowledge under our belt, we’re ready to explore more advanced topics, like handling asynchronous calls to load data, managing global application state with Redux, and moving business logic out of components and into actions, reducers, and epics. It’s about to get wiiiiiild!
As a refresher, below is a diagram of how a typical React application might be structured.
The first part of this tutorial covered the basics for the “Presentation” layer of the application. In this tutorial, we’re going to delve into the “Business Logic” and “Data” layers. But don’t worry, it’s not as boring as it sounds.
To fully understand this tutorial, you’ll want to be familiar with the concepts covered in the previous tutorial.
System requirements
As before, to build this application, you’re going to need a recent version of Node.js installed. I recommend using a tool like Node Version Manager (nvm) which allows you to install different versions of Node in parallel and switch between them. There are versions of nvm for both MacOS (or Linux) and Windows.
Getting started
We’re going to be building on the Spider game we started in the previous tutorial. If you haven’t already built the previous project and just want to jump in here, you can grab the project code from GitHub and checkout the part-2
branch. Make sure your dependencies are up-to-date, then fire up the development server.
yarn install
yarn start
Fetching data from an API
Up to this point, our application has been completely self-contained. In the real world though, you’re probably going to need to interact with a backend through some sort of RESTful API. So what would that look like in our Spider game?
How about we add some functionality to check whether or not the word/phrase entered by the user contains valid words? That seems like it could be useful. To accomplish this we’re going to use the Merriam-Webster Collegiate Dictionary API. You’ll need an API key to access the API, which you can get if you sign up for a free account.
Once you’ve gotten an API key, let’s create a service for making requests to the API. For those coming from the Angular world, you may think of a service as a class that encompasses some narrow, well-defined functionality. For our purposes though, let’s think of a service as a stateless module that encompasses some narrow, well-defined functionality. That probably sounds like pretty much the same thing. The difference is that our service does not maintain any internal state — it simply exports stateless functions.
Create a file at src/services/dictionaryApi.ts
. This file will be straight TypeScript with no JSX, so we can use the .ts
extension. We’re going to export a single lookupWord
function, that uses the Fetch API to make an HTTP request to the API and return the response.
You’ll notice we used async
/await
, which is really just syntactic sugar around Promises. You may have also noticed some weird, Node-like syntax to access some configuration from environment variables:
const API_KEY = process.env.REACT_APP_DICTIONARY_API_KEY;
const BASE_URL = process.env.REACT_APP_DICTIONARY_API_URL;
This is special functionality brought to you by React Scripts. It allows you to access environment variables, defined on the command line or in a .env
file in the root of your project. Any environment variable that is prefixed with REACT_APP_
can be accessed this way, as well as the NODE_ENV
variable which is defined by default. But keep in mind that the values are injected into the code at build-time, not at runtime.
Create a .env
file in the root directory of the project, so we can define our API base URL and key.
Now that we have our service, we can use the lookupWord
function in the handleSubmit
function of our App
component. Our approach will not be super sophisticated. Basically, we’ll split the text up into individual words and lookup each word independently. Here’s a sample of the JSON response from dictionaryapi.com, if we were looking up the word “car”:
There’s a lot more data in the response than we’ll actually use. For our purposes, we just want to iterate over each entry in the array, and check if our word exists in thestems
array defined in the meta
object. That should do reasonably well at identifying valid words. And of course, we’ll want to store a flag in state letting us know if we find any invalid words.
If you’re thinking that seems like a lot of logic to stuff into an event handler, you’re absolutely right. And we’ll address that a little later. For now though, let’s update the PlayContainer
component to take our new invalid
value as a prop and display a warning message to the user.
You may have noticed we’re creating a lot of similar divs
with various messages. As an additional exercise, feel free to encapsulate this into a Callout
component.
And now that the PlayContainer
can accept an invalid
prop, let’s update our App
component to pass the value to it.
And just like that, we’ve added another layer from our application diagram!
Bring on Redux!
So our application is working pretty well at this point. But you may have noticed that our components are starting to do an awful lot. As our application grows, that can start to make it unwieldy and hard to maintain. Redux is a predictable state container that centralizes your application’s state and logic, helping you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. Centralizing state and logic sounds like exactly what we need!
The basic principles of Redux are simple, as explained in the Redux documentation:
The whole state of your app is stored in an object tree inside a single store. The only way to change the state tree is to emit an action, an object describing what happened. To specify how the actions transform the state tree, you write pure reducers.
That’s it!
So the gist of it is this: Redux provides a single store, which houses your application state as a plain old JavaScript object. The store also provides you with a dispatcher, that allows you to dispatch actions to update your state (similar to how events are fired from components). Dispatched actions are run through a reducer function, which takes the current state object and the action as parameters, and returns a new state object that has been updated with the data from the action.
There are lots of different ways people have chosen to organize their Redux code. For this example though, we’re going to use the Ducks Modular Redux Pattern. In my opinion, this pattern makes navigating your Redux code much simpler, as it keeps everything you need for a specific module all in one place.
We’re also going to use Redux Toolkit library, which comes directly from the Redux team. It provides some useful, opinionated tools for efficient Redux development, helping you to focus on your application’s core logic without worrying about a bunch of boilerplate code.
Let’s go ahead and install Redux Toolkit. It will also install Redux and some other useful libraries.
yarn add @reduxjs/toolkit
And with that, we can create our first action and reducer! In a typical Redux application you’ll have multiple reducers, which get combined together to create a root reducer. Each reducer correlates to a key in your state object. This may be getting confusing, but stick with me and hopefully it will start to make sense.
You’ll want to divide your reducers up in a way that makes some organizational sense. Since we’re managing the global state for our UI, it might make sense to divide it up by route/view. If we take that approach, we could structure our global state object like this:
{
play: { },
start: { }
}
And then if we break down our /play
route, we can identify what pieces of data we need for its state.
{
play: {
text?: string;
usedLetters: string[];
},
start: { }
}
Let’s dig into it by creating a new file at src/redux/modules/play.ts
. In that file we’re going to define a new setText
action creator that we can use to dispatch an action to update the word/phrase to guess. An action is just an object with a type
field to identify the action, and an optional payload, which can be any type you want. A traditional action creator is a simple function that returns an action object.
We’re also going to define our reducer function, which takes the current state and action as parameters, and returns the updated state object. This traditionally involves a switch
statement that keys off of the action type.
As you may have noticed, that’s a fair amount of code to write just to set a single string value in our state object. This is where Redux Toolkit starts to shine. They provide some utility functions, like createAction
and createReducer
, to cut down on the boilerplate.
Now doesn’t that look nicer? It also gives the added benefit of better type support in our reducer functions, because the compiler is able to infer the type of payload
from the action. Yay!
It’s also worth mentioning that the createReducer
function from Redux Toolkit uses immer under the hood, which allows you to write reducers as if they were mutating the state directly while still maintaining pure reducer functions. So in our reducer function above, it would have been completely safe to write it this way (I just prefer the more deliberate syntax):
builder.addCase(setText, (state, { payload: text }) => {
state.text = text;
});
With our first reducer in place, let’s move on to setting up our store. First we’ll want a root reducer to combine the reducers from each of our Redux modules. Right now we only have one module, but that’s OK because this is just a trivial example for learning purposes (or at least, that’s what I’ll keep telling myself). Create a file at src/redux/root.ts
.
Not much to see here. The usefulness of combineReducers
is more obvious as your application grows and you add additional reducers. But for now, we’ve got the structure in place to support a growing application.
Next, let’s configure our store. Create a file at src/redux/configureStore.ts
.
Again, not much to see here. We’re passing a configuration object to the configureStore
function that allows us to specify our root reducer. Later on we’ll do some additional stuff, like configure middleware — but let’s not get ahead of ourselves.
So what now?
OK. We’ve created our first action and reducer. We’ve configured our store. But how do we use it in our application? Redux is designed to work in any JavaScript application. We could interact directly with the store
object we created, which exposes a dispatch
function and a getState
function. However, React Redux makes integrating Redux into your React application much, much simpler.
Before we can use it, we’ll need to install it:
yarn add react-redux# Add types for better TypeScript supportyarn add -D @types/react-redux
React Redux has already done the work of tying state changes in your Redux store to React’s change detection cycle. To tap into this, we’ll need to wrap our App
component with React Redux’sProvider
component and pass our store to it. We can do that in our index.tsx
file.
With our Provider
in place, we can start interacting with our store from our components. Traditionally, this was done through a higher-order component (HOC) that you would create with the connect
function. The connect
function allows you to provide mapping functions as parameters, that map state values and action dispatchers to the component’s props. That way, you’re interacting with the store via your component’s props.
For example, our App
component would look something like this:
However, with the introduction of React Hooks, the React Redux team added their own custom hooks. So now it’s possible to access any piece of global state inside your component with useSelector
, and dispatch any action with useDispatch
— no HOCs required. So with the hooks, our App
component looks like this:
Let’s do this!
Hopefully Redux is starting to make more sense now that you’ve seen it in action. At this point we’re ready to take all our application state and move it into our Redux store, right? Well, not exactly.
You want to be deliberate and pragmatic about what data goes into your Redux store. Keep in mind that every action that is dispatched will run through every reducer. Things like form data are not good candidates for Redux, because a controlled component is going to fire off a state change with each keystroke. The Redux FAQs outline some rules of thumb to help you determine what data belongs in Redux:
Do other parts of the application care about this data?
Do you need to be able to create further derived data based on this original data?
Is the same data being used to drive multiple components?
Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?
Do you want to cache the data (ie, use what’s in state if it’s already there instead of re-requesting it)?
Do you want to keep this data consistent while hot-reloading UI components (which may lose their internal state when swapped)?
So with that in mind, let’s take a look at our application. In our PlayContainer
component, there are a couple of easy candidates: usedLetters
and maxIncorrect
. First we’ll add the appropriate action creators and update our reducer:
Then use it in our PlayContainer
component:
Selectors and derived state
Looking at our use of the useSelector
hook, you’ve probably been able to figure out what a selector is in the context of Redux. It’s really just a function that takes the state object as a parameter, and selects the desired value from it.
Up to this point we’ve been using anonymous functions for simplicity’s sake, but that approach can be problematic as the application grows. Ideally, we don’t want our components to need to be aware of the shape of our state object. If down the road we decide to move some things around in our state object, we don’t want to have to touch each component that relies on that piece of state. So how do we fix that? Well, a selector module should do the trick.
Create a file at src/redux/selectors/play.ts
. In this file, we’ll export functions to select the pieces of state we’re using so far.
Then we can update our App
and PlayContainer
components to use our shiny, new selectors.
So now we have selectors, and they provide value with some degree of future proofing our components. But you may still find yourself asking, “Are they really worth it?” Well, before you pass judgement, let’s talk about derived state.
Derived state refers to values that are computed from other values in state. As an example of this, we can look at the textLetters
object in the PlayContainer
component. It’s an object that maps the unique letters in our text, and is derived from the text
value coming out of our Redux store. The same is true of our correctCount
and incorrectCount
values, which derive their values from the usedLetters
array coming out of our Redux store, along with the previously derived textLetters
object.
There are a few different ways we can manage this kind of derived state. One option would be to leave it as-is. But that’s not very sharable across components. Another option would be to calculate the derived values in our reducer and store them in the state object. That’s definitely more sharable, but it can bloat our state object, and adds more overhead to our reducer. A third option is to create a selector function that uses your existing selectors to get the relevant pieces of state, then returns the derived value. That’s sharable, and doesn’t bloat our state object … but what about performance? Remember, selectors are called all the time as part of React’s change detection cycle to determine if any state values have changed. We certainly don’t want to perform those computations repeatedly — potentially tens or hundreds of times per second. But what if we take some inspiration from our original solution, using memoized functions? Well now we’re getting somewhere!
As luck would have it, a solution already exists. Reselect is a simple library that provides the ability to create memoized selectors. And get this: it’s already included with Redux Toolkit!
The createSelector
function takes an array of selectors as its first argument, and passes the values from those selectors as arguments to a selector function. The resulting selector is memoized, so the function is only executed when one of the dependent selectors returns a new value. Neat, huh?
Let’s add some memoized selectors to our src/redux/selectors/play.ts
file:
And then update our PlayContainer
component to use our new memoized selectors:
As you can see, the amount of logic in our component is decreasing drastically — which is a good thing.
As an additional exercise, create a memoized selector to get the current step
value.
Are you thunking what I’m thunking?
So now we have Redux working in our application. And we’ve reduced the computation of derived state in our components through selectors. But we’ve still got a lot going on in our components. That handleSubmit
function in our App
component is a prime candidate for simplification. But how can we move that logic into Redux? As you are aware, that function makes HTTP requests to the dictionary API. But one of the Three Principles of Redux states that “Changes are made with pure functions.” So how do we handle asynchronous code in Redux? One word (although it kind of sounds like two): middleware.
Middleware is the suggested way to extend Redux with custom functionality. It allows you to tap into your store’s dispatch
function, so you can interact with — and even modify — your actions before they reach your reducer. Any side-effects triggered from actions (such as HTTP requests) should be handled in middleware.
But writing middleware is complicated, isn’t it? Not when it already exists! And especially not when it’s already included with — and configured by — Redux Toolkit. The Redux Thunk middleware allows you to write action creators that return a function instead of an object. A thunk is just another name for a function that is returned by a function. In the case of Redux Thunk, the function returned by your action creator takes the dispatch
and getState
functions as parameters, which then allows you dispatch additional actions or access the current state object. The Redux Thunk middleware checks each action as it is dispatched and if the action is a function (a thunk), rather than an object, it will execute the function with the dispatch
and getState
functions from the store.
So how does that help with HTTP requests? We can dispatch an action to make the HTTP request. When the request completes, we dispatch another action with the result. Is that about as clear as mud? Well, let’s try adding it to our application and see if that brings any clarity.
That may seem like a lot to take in, but let’s break it down.
First, we refactored our setText
action creator to call it updateText
. This is because we want to use the setText
action creator for our thunk, but we still need an action that can tell our reducer to update the text
value in our store.
We also added an invalid
flag to our state, along with an action creator for setting the flag, and the associated reducer function.
Lastly, we added our setText
thunk. The actual functionality looks almost identical to what it looked like in the App
component. Notice how there are multiple places where additional actions are dispatched. That allows us to update individual pieces of our state as the asynchronous operations complete, as well as wait for certain asynchronous operations to complete before updating other values in the state.
Now we can update our StartContainer
component to use the new setText
action creator:
Our PlayContainer
component can also be updated, since it can now get everything it needs from the Redux store, rather than relying on the App
component to manage global state.
And lastly, our App
component can forget about managing any state whatsoever.
Once you’ve made all these changes, you may notice that the “Start Over” button no longer works. This has to do with the fact that our StartContainer
component is now triggering a redirect to the /play
route if text
is set in state. We’ll need to update the “Start Over” button to clear the text
value, which will then trigger the navigation to the /start
route (and stay there until text
is set again).
And just like that, we’ve rewritten our entire application to make it look and function exactly the same! But seriously, we’ve made some real improvements by structuring our application to have a much clearer separation of concerns, and isolating our business logic so it can be more easily shared and tested.
Turning it up to eleven
At this point we have the foundation for a scalable, enterprise React application. So we’re done, right? Well, not quite. Let’s take it to the next level and get reactive!
RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides types (Observable, Observer, Schedulers, Subjects) and operators (map, filter, reduce, every, etc.) to allow handling asynchronous events as collections. As the RxJS team puts it:
Think of RxJS as Lodash for events.
The power of RxJS comes from its ability to compose streams of data or events and transform them using pure functions to produce values that react to — or update based on — new data or events being pushed down from the streams.
If you’re not familiar with RxJS at all, it can be a little daunting at first. But don’t let that discourage you. Hopefully you can get at least the general idea from the sample code. You can also checkout learnrxjs.io, which is a pretty great resource for getting started with RxJS.
To incorporate RxJS into our application, we’re going to use the Redux Observable middleware. Redux Observable allows us to treat dispatched actions and current state as streams, which are handled through epics. Epics are just functions that take two Observables as parameters: one representing a stream of actions and the other representing a stream of the current state from the Redux store (they can also take a third argument, which is an object of injected dependencies, but we won’t get into that here). Within an epic you use RxJS operators to perform side-effects, transform data, and ultimately return a stream (Observable) that pushes new actions.
But enough talk, let’s get to it! Start by installing the Redux Observable library, along with RxJS:
yarn add redux-observable rxjs
Now we’re going to create an epic in our src/redux/modules/play.ts
module. Remember, an epic is a function that takes an Observable of actions and optional Observable of state as parameters, and returns an Observable of actions — actions in, actions out.
Isn’t that so much better?!
OK, OK. I’ll admit this probably just looks like a more complicated way to do exactly what we were doing before. It’s hard to demonstrate the real power of RxJS in such a trivial example. But let’s at least go through it, then explore some ways that we can start to see the benefits of RxJS.
So the first thing we did was get rid of our setText
thunk, and re-renamed our updateText
action creator back to setText
. Then we created a setTextEpic
function. For this example we don’t need to access the current state, so we only included the first parameter, the Observable of actions. Our epic needs to return an Observable of actions, so we’re just going to return the current action$
Observable, using the pipe
function to chain operators for transforming the data and performing side-effects.
Let’s step through our operators and see what they’re doing. First we use the ofType
operator. This is a simple filter that will only emit actions down the chain that match the specified type. Typically an epic should be focused on handling a single action, and you write lots of epics to handle your various actions.
Next, we’re using the filter
operator to ensure that we have a new value for the text before we continue down the chain. Filter will only emit actions that match the specified criteria. In our case, it will prevent us from performing all the validation logic when we’re clearing our text. It’s worth mentioning we could have combined ofType
and filter
into a single filter
, but I wanted to demonstrate the ofType
operator. Also be aware that actions don’t arrive to your epics until after they’ve gone through your reducers. According to the Redux Observable documentation:
Epics run alongside the normal Redux dispatch channel, after the reducers have already received them — so you cannot “swallow” an incoming action. Actions always run through your reducers before your Epics even receive them.
So even though we aren’t processing any empty values in our epic, the text
value in our store will still get updated when we clear it. Note that we also updated our reducer to convert our text to uppercase (rather than in our action), since it will reach the reducer before the epic.
After that, we use the switchMap
operator which allows us to return an Observable (or Promise), which we’ll need to call our asynchronous lookupWord
function. Inside the switchMap
operator, things probably look very similar to our Promise-based solution. There are a couple of key differences: 1) We wrap our lookupWord
calls with the from
function, which creates an Observable from a Promise. And 2) we use the forkJoin
function rather than Promise.all
, which in a similar fashion takes an array of Observables and will give us a single Observable that emits an array of results when all of the Observables that were passed in emit a value.
At the end of it all, we emit a new action by returning the setInvalid
action. It’s imperative that your Observable emits a new, and different, action. Otherwise you’ll create an infinite loop within your epic, which is generally considered to be a bad thing.
Now that we have an epic, we need to configure the Redux Observable middleware (at least, if we want it to work). First, we need to create a root epic. Similar to how Redux only allows a single root reducer, Redux Observable requires a single root epic. For convenience, they provide a combineEpics
function which, similar to Redux’s combineReducers
function, wraps any number of epics in a single root epic.
Let’s export our root epic from the same module as our root reducer, src/redux/root.ts
.
And then we’ll configure the middleware in src/redux/configureStore.ts
.
It’s worth pointing out here the call to getDefaultMiddleware
. This allows us to get the middleware already configured by Redux Toolkit, and add the epic middleware to it. Otherwise, we’d lose the middleware provided by Redux Toolkit.
You’ll also notice that after we configure our store, we make this call:
epicMiddleware.run(rootEpic);
That’s what starts up the actual processing of your epics alongside the normal Redux dispatch channel. And with that in place, we now have a fully functioning epic in our application!
So why RxJS?
As I mentioned earlier, RxJS can be overwhelming at first, and it’s hard to see its real value in such a trivial example. So let’s try adding some functionality that has real value and hopefully shows some of the advantages of RxJS.
Suppose, for the sake of this example, our requests to the dictionary API take a very long time … perhaps there’s some issue with the API or our user has a very slow internet connection. Then also suppose that a user enters in a sentence which ships off a bunch of HTTP requests, then decides to change their sentence. When they click “Start Over” and enter in a new sentence, it will ship off another batch of HTTP requests. But if the first batch of requests haven’t completed yet, the new batch will just get queued up and wait for the others to complete first. Not to mention, when the first batch does complete the responses will run through our reducers even though we no longer care about those responses.
So what can we do about this? One of the big advantages of Observables over Promises is that Observables are cancellable. So in theory, we should be able to cancel our requests when the user navigates away from the /play
route.
The first thing we’ll need to do is update our dictionaryApi
service to use the RxJS fromFetch
function, rather than the global fetch
. The fromFetch
function is a simple wrapper around the global fetch
function that returns an Observable rather than a promise. Additionally, it automatically sets up an AbortController, which allows us to not only cancel the subscription on the Observable, but cancel the HTTP request.
Next, we’ll need to update our setTextEpic
so it knows when to cancel any pending requests. The Redux Observable documentation gives us a recipe for cancellation that uses the takeUntil
operator to trigger a cancellation when a cancellation action is dispatched.
As you can see, we’ve added a cancelSetText
action creator, as well as adding the takeUntil
operator to our forkJoin
. The takeUntil
operator takes a notifier Observable as its only parameter. It emits the values from the source Observable until a value is emitted from the notifier Observable, at which point it completes, effectively cancelling our upstream Observables including any pending HTTP requests. For our notifier Observable, we’re simply going to use our action$
stream and filter for our cancelSetText
action. So now if we dispatch the cancelSetText
action, it will cancel any pending requests. Pretty slick!
The last thing we need to do is update our PlayContainer
component to dispatch the cancelSetText
action when a user navigates away. We can do this by tapping into our existing useEffect
hook. The useEffect
hook already has a built-in clean-up mechanism. By returning a function from our useEffect
hook, React will automatically run that function any time the component unmounts (such as when navigating away). So we just need to return a function that dispatches our cancelSetText
action.
Although still a somewhat trivial example, I hope you’re starting to see the advantages we gain through RxJS.
If you’re feeling ambitious, as an additional exercise try adding retry logic with an exponential backoff to your API requests using RxJS. (Hint: check out the retryWhen
operator.)
We did it!
Wow, what a ride! We’ve covered making asynchronous calls to fetch data; using Redux to manage global application state; using memoized selectors to efficiently compute derived state; using Redux Thunk for dispatching simple actions with side-effects; and took things to the next level for more complex state management with RxJS and Redux Observable.
At this point, we’ve covered most of our business logic and data layers of our application diagram.
For those looking for an additional challenge, take your new found knowledge of Redux middleware and try persisting your gameplay state in local storage with Redux Persist.
I hope these tutorials have helped you learn some useful patterns and practices to help you build scalable, enterprise-ready React applications in the real world. Now go and build something awesome!