Building a React Application: Part I
React is a powerful JavaScript library whose primary focus is on rendering fast, performant UIs, using one-way data-binding to populate the view. That might just sound like a bunch of technical buzzwords because, well, it is. The main takeaway from all those buzzwords is that React is intended for building UIs. In order to build a large application, you’ll likely need more than just React. Below is a diagram of how a typical React application might be structured. As you can see, React is really only the leftmost part of the diagram.
That being said, there’s still a whole lot that can be built with just React. And the flexibility that comes with React can make it challenging to get it right.
In this tutorial, we’re going to lay the ground work for building a basic React application. We’ll cover some core concepts, like bootstrapping your application with Create React App, building components, managing state using React Hooks, and configuring routing with React Router.
To fully understand this tutorial, you’re going to need some familiarity with JavaScript. We’re actually going to write our application code in TypeScript, because TypeScript is awesome! But if you’ve never written any TypeScript code before, don’t let that scare you off. TypeScript is often referred to as syntactic sugar for JavaScript. It’s actually a superset of JavaScript which compiles down to plain JavaScript (so plain JavaScript is technically valid TypeScript, but TypeScript is not necessarily valid JavaScript). This MSDN article, Understanding TypeScript — although a little dated — gives a good overview of TypeScript, particularly for C# and JavaScript developers.
You’ll also want have a minimal level of comfort working with command line tools. In this particular case, using dependency management tools for Node.js, like npm or Yarn.
System Requirements
To build this application, you’re going to need a recent version of Node.js installed. You can download the installer directly from their website. However, I prefer 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.
Application Requirements
Before we can build an application, we need to understand what it is we’re building. For this tutorial, we’re going to build a game called Spider. For those who think they’ve never played this game, it’s really just Hangman, but less inappropriate since you draw a spider hanging from its web rather than a human.
Our requirements are as follows:
- A person playing the game can enter a word or phrase to be guessed
- Text cannot be seen as it’s typed
- Letters of the word/phrase to guess are displayed as blanks (underscores) until guessed
- Other characters in the word/phrase (e.g. spaces, apostrophes, etc.) are displayed
- Letters to guess (all letters of the alphabet) are displayed on screen as buttons
- Clicking a correct letter disables the button and displays the letter in the word/phrase
- Clicking an incorrect letter disables the button and displays incorrect indicator
- Structure from which the spider will hang is displayed on screen
- With each incorrect guess another part of the body is filled in: abdomen, cephalothorax, individual legs
- Game can be reset at any point
The completed project is available in this GitHub repository, with branches you can check out for the various steps of the tutorial. I’d recommend writing the code yourself though, so you have a chance to get more familiar with it. You can also view a working version of the final application at https://ericgibby.github.io/spider-app.
Getting Started
For this project we’re going to use Create React App (CRA) to bootstrap our application. As stated on their webpage:
Create React App is an officially supported way to create single-page React applications. It offers a modern build setup with no configuration.
We’re going to use it because it’s quick, it’s easy, and it comes directly from the React team.
We’re also going to use Tailwind CSS for styling our application. Tailwind CSS is a utility-first CSS framework, which gives us a lot of flexibility in how we build and style our application. If that doesn’t mean anything to you, that’s OK. You can read more about using CSS utility classes in Adam Wathan’s article, CSS Utility Classes and “Separation of Concerns”.
To get started, let’s head to the terminal or command prompt and run the following command:
npx create-react-app spider-app --template craco-tailwindcss-typescript
Let’s break down this command:
npx
is a package runner, which allows us to run a package binary without having to install it first.create-react-app
is the package we’re running. It will do all the heavy lifting of stubbing out our project, configuring dependencies, and setting up our build tooling.spider-app
is the name of our application. CRA will create a folder with this name in the current directory, and place all the project files inside that directory.--template
tells CRA what project template to use when creating this project. CRA ships with a couple of built-in templates: the default template which uses JavaScript, and thetypescript
template which uses TypeScript. For this project, since we’re using Tailwind CSS, there is some additional configuration required to get that set up. Rather than going through that here, I’ve created a custom template which handles that configuration for us with thecraco-tailwindcss-typescript
template.
At this point, you should have a fully functional React application. You can run these commands to fire up the development server, open your browser, and load the application:
cd spider-app
yarn start
Now let’s take a quick look at our project structure (for more information, refer to the documentation). Inside the spider-app
directory, you’ll find a bunch of configuration files which you can — for the most part — just ignore. Inside the public
directory you’ll find the index.html
file, which is the document that loads in the browser when a user navigates to the application. In the src
directory, you’ll find our TypeScript source files. index.tsx
is the entry point for the application. And this part inside index.tsx
is where the magic starts:
This is telling React to render the App
component inside the DOM element with id="root"
. So basically, everything inside that root element is going to be controlled by React.
So now that we have a working React application, let’s get into the code!
Components and JSX
React is a component-based library for creating UIs. It allows you to build encapsulated components that manage their own state, then compose them to make more complex UIs. So a React application is really just a tree of individual React components.
Components can be written in React as either classes or functions. For this tutorial we will be writing function components. Function components are more concise, and are less prone to common pitfalls like storing internal state in instance variables, using inheritance for code reuse, or putting business logic in class methods. If you want to learn more about class components, refer to the documentation on the React website.
Function components are just what they sound like: a function. A function component takes a single argument, which is an object representing the properties or “props” for the component. Props are just data being passed to the component. The component is then in charge of displaying that data in some way, or passing it down to other components. In the end, your function returns React elements describing what should appear on the screen.
If you open up App.tsx
in your IDE, you’ll notice the file contains HTML markup inline with the TypeScript code.
Note that it is not a template string containing markup, but rather the markup is part of the TypeScript code. This is known as JSX (XML in JavaScript), which is a syntax extension to JavaScript. While not required for writing React code, it is recommended and makes life a lot easier.
Let’s write some JSX by deleting the sample code in App.tsx
and replacing it with a simple heading for our game:
We can also get rid of some of the the other sample content files, such as the logo.svg
file and App.css
since we no longer need any custom styles for our app component.
Notice that as you make changes and save your files, your browser will automatically load the updated content. This is thanks to react-scripts, which was configured by CRA and uses the webpack’s DevServer under the hood.
Our First Component
Now that we have our application shell, we can start to create the various building blocks, or components, that will make up our application. Let’s start with our first requirement: “A person playing the game can enter a word or phrase to be guessed.”
For this, we’re going to need a form with a text input and a submit button. We’ll take an atomic design approach, and start with small, focused components that can be composed together. With that in mind, we’ll create a simple Button
component to start.
React projects can be structured however you like. My preference is to create a components
directory inside the src
directory, and then place directories inside that for each new component. The individual component directories can contain the component source file and any other supporting files, such as CSS, tests, and utility functions specific to that component.
Create a new file at src/components/Button/Button.tsx
. This component will take props for specifying the button type, the button text, and an onClick
handler. We will also add some default styles to the button using Tailwind CSS’s utility classes.
Tailwind CSS is beyond the scope of this tutorial. As an additional exercise, feel free to play around with customizing the style of your button.
Notice the use of camelCase
property names for HTML attributes like class
and onclick
. According to the React website:
Since JSX is closer to JavaScript than to HTML, React DOM uses
camelCase
property naming convention instead of HTML attribute names.For example,
class
becomesclassName
in JSX, andtabindex
becomestabIndex
.
You may also notice the use of curly braces inside the JSX. Curly braces can contain any JavaScript expression. In the case of the onClick
and type
attributes on the button
element, it is binding the values from theButton
component’s props to the attributes on the element. In the case of {text}
it is simply rendering the string passed in through the text
prop inside the button
element.
Now add your Button
component to the App
component and see your snazzy new button rendered on the page. You can even add an event handler function to the onClick
prop of your button to see it in action. Notice how we can define an arrow function directly inside the curly braces, because curly braces can contain any JavaScript expression.
Form Controls
Now that we’ve mastered the basics of a component, let’s create something a little more complex: a text input component.
Create a new file at src/components/TextInput/TextInput.tsx
. This component will take props for specifying the input type, the value, an optional label, and an onChange
event handler.
In this component, we have some conditional content that only renders if a value is specified for the label
prop. We’re able to use the inline conditional operator &&
because in React, if an expression evaluates to undefined
, null
, or false
, then nothing is rendered.
Now you can plug your new TextInput
component into the App
component and see how it looks.
You may have noticed there are a couple of props we forgot to set on our TextInput
component: onChange
and value
. Go ahead and set a value on the component:
<TextInput label="Enter a word or phrase" type="password" value="Hello world!" />
If you look in the console in your browser’s developer tools after making that change, you’ll notice React has given you a very helpful error:
Warning: Failed prop type: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
This makes sense, because we’re telling the input
field what its value should be, but we’re not providing a way for that value to ever change. By specifying the value through props, we’re putting React in control of managing the state of the input
element. We need to add an onChange
event handler so we can update the state of the input each time it changes. This is what is referred to in the React world as a “controlled component.” This also happens to be the recommended way to handle form elements, such as input
, textarea
, and select
, in React. There can be use cases where “uncontrolled components” make more sense, but that’s outside the scope of this tutorial.
For now, go ahead and take the value
prop off of your TextInput
component. We’ll implement a real solution next.
Managing Component State
Up to this point, the components we have created are stateless — they simply take inputs through props and output events. Those types of components can be referred to as “presentational components,” because their only concern is to present the data they’re given from their props.
Several years ago, Dan Abramov wrote an article, Presentational and Container Components, in which he discusses a pattern of separating concerns by creating stateless presentational (or “dumb”) components, and stateful container (or “smart”) components. One is focused on displaying the data, while the other is focused on managing the data. While he states that his views have since evolved, and warns about being dogmatic in following the pattern, it can still be a useful pattern to understand and be mindful of as you build your React applications.
Let’s make our first container component, a TextInputForm
component that can pull our TextInput
and Button
components together. Create a new file at src/components/TextInputForm/TextInputForm.tsx
. This component will take a single onSubmit
prop, which is a handler that receives the value from the form. We also want to set the type
prop on our TextInput
to password
to meet our second requirement: “Text cannot be seen as it’s typed.”
Then drop our new TextInputForm
into our App
component so you can see it in action.
So far, this is nothing new. But you’ll notice the handleSubmit
handler function on the form
element isn’t actually passing any of the form values to the onSubmit
function. So how do we get the values from our TextInput
? Well, that part’s easy. We can wire up our onChange
handler on the TextInput
component.
So now we have the value. But how do we hang onto it? Remember, your component is a function that gets called any time React detects a change. This is known as re-rendering, and it happens a lot because React can do it very quickly and efficiently. But how do you get a value to stick around between renders? Storing it in a variable inside the component function won’t last between renders. Storing it in a variable outside the function makes the value global to all instances of the component, which isn’t very reusable. Enter React Hooks.
According to the React website: “Hooks are functions that let you ‘hook into’ React state and lifecycle features from function components.” There are a number of extremely useful hooks that ship with React. The useState
hook provides a means for managing internal component state. It allows you to store a value that persists across renders, and plays nice with React’s change detection cycle (meaning, your component is aware of when the value changes and will update accordingly). For our particular problem, it will give us exactly what we need. Calling useState
returns an array that contains the currently stored value, as well as a function for updating the stored value. With that in place, we can properly hook up our onChange
handler and value
prop on the TextInput
component, as well as properly pass the current value to the onSubmit
function.
As an additional exercise, try adding a toggle to show/hide the text in the TextInput
.
There’s a lot more to hooks that we won’t cover here. I highly recommend reading the hooks documentation. It’s also worthwhile to understand the rules of hooks as outlined by the React team. To be clear, hooks aren’t the only solution. But they’re tailored toward function components, which fits our project nicely. If you’re interested in solutions for class components, you can read more in the documentation.
If we refer back to the diagram at the beginning of the tutorial, we’re starting to get a clearer picture of how the various pieces work together.
We’ve got our TextInputForm
component, which serves as our container, managing state and passing it down to our presentational components: TextInput
and Button
. Our presentational component can then fire off events which get handled in our container, where it can update the state accordingly.
Routing
OK. We’ve got a form that allows us to enter in a word or phrase to be guessed. But what happens after the user submits the form? Let’s navigate to a new page where the user can now guess the word/phrase.
Routing in a single-page application can get complicated if you’re trying to manage it all on your own: keeping track of URL changes, parsing parameters from the path and query string, using the browser’s History API, blah, blah, blah. Fortunately, React Router makes routing dead simple. It provides a set of components for declarative routing, and custom hooks for interacting with the History API.
Add React Router to your application with this command:
yarn add react-router-dom# Add types for better TypeScript supportyarn add --dev @types/react-router-dom
Now let’s add some routing. First, we’ll need to wrap anything that needs access to routing inside the BrowserRouter
component. We’ll do that in our index.tsx
file. Then we’ll add some Route
components to our App
component. Each route component renders its contents if the URL matches the path specified in its path
prop. We’ll have our text entry form live at /start
.
We’ll also want a catch all route (path="*"
), so if a user enters an invalid URL we can redirect them back to the /start
route. We can do that with the Redirect
component. But we’ll only want our catch all route to render if no other routes are matched. Otherwise, we’ll end up with an infinite redirect — which is not a great user experience. Fortunately, React Router also provides a Switch
component, which works similarly to a switch
statement in code. The Switch
component renders the first Route
or Redirect
component that matches the location, so we’ll only hit our redirect if the location doesn’t match any of our other routes.
This is pretty good so far. But for organizational purposes, I prefer to create container components for each route. Create a file at src/containers/StartContainer.tsx
. This component will contain everything that renders with the /start
route.
You may have noticed this component uses a weird, empty tag (<>…</>
) to wrap the contents. This is a shorthand syntax for the React.Fragment
component. Since a React component can only return one top-level element, React.Fragment
allows you to wrap multiple elements without adding extra elements to the DOM (in other words, fewer div
s).
You may also have noticed this use of the spread syntax: <TextInputForm {...props} />
. Remember, JSX represents actual objects in JavaScript/TypeScript, so this is a valid way to merge props without having to specify each and every prop individually.
Let’s also stub out a PlayContainer
, so we’ll have a place to house our gameplay components. Create a new file at src/containers/PlayContainer.tsx
.
Now that we have our StartContainer
component, let’s update our App
component to use it, and add a /play
route that uses our PlayContainer
component.
With multiple routes in place, we’re ready to add some navigation to our app. There are a couple of ways to add navigation with React Router: programmatically using the history
object, or declaratively using the Link
or NavLink
components.
We’ll start by programmatically navigating from /start
to /play
after the user enters their word/phrase in the form. We can access the history
object from React Router using the useHistory
hook. Then it’s as simple as calling history.push('/play')
in our form’s submit handler inside our App
component.
Now let’s add a link in our PlayContainer
to get back to the /start
route.
And just like that, we’re able to navigate back and forth.
Building on What We’ve Started
So we’ve got a React application with some components, we’ve figured out how to manage state in our components, and we have routing set up. But we still can’t actually play the game. Let’s build out some more components for our gameplay.
Our next two requirements are:
- Letters of the word/phrase to guess are displayed as blanks (underscores) until guessed
- Other characters in the word/phrase (e.g. spaces, apostrophes, etc.) are displayed
We can create a component that handles that for us. Create a file at src/components/MaskedText/MaskedText.tsx
. This component will take text
(the word/phrase to guess) and usedLetters
(an array of characters that have already been guessed) as props. It can then parse the text and determine which characters should be displayed and which should be replaced with underscores.
At first glance, there appears to be a lot going on in this component. You’ll notice we have a getCharacters
function defined outside the component. This helper function does not rely on any state internal to the component, so there’s no reason to define it inside the component (where a new instance would be created every time the component renders). By extracting this logic into a separate function, we can also test that code independently from the UI.
Inside the component, we’re iterating over the words and letters to build up a collection of elements for displaying the text. Notice inside the map
and reduce
functions that each element we add to the collection includes a key
prop. This is necessary to give the elements inside the array a stable identity, so React can identify which items have changed, are added, or are removed.
Another thing to point out in this component is that the getCharacters
function is called each time the component renders — even if nothing has changed. While not egregious, this does have a performance impact. So how do we fix it? Hooks to the rescue!
The useMemo
hook allows us to memoize expensive calculations during the render cycle. It takes a “create” function and an array of dependencies. Any time one of the dependencies changes, the function will execute again. Otherwise, it can return the cached value. So now it only calls getCharacters
when text
or usedLetters
changes.
Now we can drop our MaskedText
component in the PlayContainer
, and add some state management to our App
component.
🎵 Just Keep Coding, Just Keep Coding! 🎵
Now we’re making some progress! So what’s next on our list of requirements?
- Letters to guess (all letters of the alphabet) are displayed on screen as buttons
- Clicking a correct letter disables the button and displays the letter in the word/phrase
- Clicking an incorrect letter disables the button and displays incorrect indicator
Let’s create a LetterButtons
component that displays a button for each letter of the alphabet. It can take text
and usedLetters
as props, just like our MaskedText
component. And it can take an onClick
handler that gives the letter that was clicked. Create a file at src/components/LetterButtons/LetterButtons.tsx
.
Then add your LetterButtons
component to the PlayContainer
component. We can add an onClick
handler and use the useState
hook to keep track of letters that have been clicked.
You may have noticed this in our handleClick
function:
setUsedLetters(previous => [...previous, letter]);
Rather than just passing in the new value, we gave it a function that takes the previous value as its argument and returns the new value. Keep in mind that setting state is an asynchronous action in React. Any time you need to update a value in state based on its previous value, you should follow this callback pattern to ensure it has the most current value when the state is actually updated.
It’s also worth mentioning that you should never mutate values stored in state. Rather than pushing the latest letter onto the usedLetters
array, we used the spread syntax to create a new copy of the array and append the latest letter. This is because React’s change detection uses reference equality (===
), so it may not re-render properly if the reference to the array doesn’t change.
Almost There!
Let’s see what’s left on our list of requirements:
- Structure from which the spider will hang is displayed on screen
- With each incorrect guess another part of the body is filled in: abdomen, cephalothorax, individual legs
We can create a Spider
component that takes a step
prop to indicate which image display. We’re just going to use a series of images that show more of the spider drawn with each image. They’re all the same size so we can swap in the appropriate image after each guess. You can download these SVG images, or feel free to create your own (I created mine on excalidraw.com). Just place them in the src/components/Spider
directory, and create a Spider.tsx
file along side them.
You can drop this into the PlayContainer
component, but you’ll quickly notice that we’re going to need a count of incorrect guesses to know what to pass to the step
prop of our Spider
component. That’s easy enough to get, since it can be derived from our text
prop and our usedLetters
stored in state.
Because there are 10 different spider images, the game will be pretty easy. As an additional exercise try adding optional difficulty levels (e.g. one that adds the legs two at a time, or one that adds 4 legs at a time).
At this point we have a mostly working game. There are a few clean-up items we’ll want to take care of. First, let’s indicate when someone wins or loses, because that seems like a useful thing for a game to do.
And finally, let’s make sure we send the user to the /start
route if they arrive at the /play
route without a word/phrase already set. For this we can use another React hook, useEffect
. This hook is intended for actions that cause side-effects while rendering the component — like redirecting away from the page.
It’s important to include the second argument in useEffect
, the dependency array, because that’s how React determines if it needs to run that effect in the current render cycle. If none of the dependencies have changed, it won’t run.
We Did It!
Well, we did it. We created a React application with Create React App; built presentational components and container components; used React hooks to manage state, memoize expensive calculations, and perform side-effects; and handled routing with React Router.
It seems like a lot, but so far we’ve only covered the presentation layer of our application diagram.
Up next, in Building a React Application: Part II, we’ll explore some 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.