React from the Beginning
- Reference
- React 101
- Starter notes (NodeJS,
create-react-app
, React docs, etc.) - What React is and why we need it
- First React program (and touching on JSX and Babel)
- More JSX and Babel
- Understanding React elements as opposed to DOM elements
ReactDOM.render()
and the virtual DOM- Components
- Props
- Multiple components in an array (and
unique "key" prop
warning) - Components as classes
- React Essentials: Recap
- Starter notes (NodeJS,
- State and Events (and
create-react-app
)npx
vs.npm
in regards tocreate-react-app
(usenpx
!)- Modernizing our web development with
create-react-app
- Understanding the file structure given to us by
create-react-app
import
a new component intoApp.js
before you make it so you can benefit from the linter- What is state?
- A note about
React.StrictMode
double-invoking functions (constructor
,render
, etc.) - Getting started with state (where to initialize, do's and don'ts, etc.) and events (binding in
constructor
, etc.) - Events in React
- Available events
- Changing state with an event
- Practicing with managing state with events
- State and props together
- Stateless componenets vs stateful components (terminology)
- The Component Lifecycle and HTTP
- server-side rendering vs. what we typically aim to do with React
- HTTP requests using React
- Note about environment variables and hiding secrets (e.g., API keys) in React
- The component lifecycle
- Reference: The docs on
React.Component
(in a nutshell) - The Lifecycle:
componentDidMount()
- The Lifecycle:
render()
- The Lifecycle:
componentDidUpdate()
- The Lifecycle:
componentWillUnmount()
- Practicing what we know so far: city weather application
- Controlled forms: Managing forms with state
- Handling multiple inputs with one
onChange
event listener - Data flows down so pass state up!
- Styling components
- React Router
- Introduction to React Router and why you may want to use it
- React Router docs reference (for web applications)
- Getting started:
Router
,Route
, andLink
NavLink
and making aNavBar
componentRoute
s with thecomponent
prop vs therender
prop- Nested routes
- The docs on
Route
component props:match
,location
, andhistory
- Using
Route
component props in our application - Using the
match
prop onRoute
for dynamic URLs and URL params
- Redux
- Redux docs reference
- React Redux docs reference
- Before Redux and React-Redux
- What is Redux?
- Redux and React
- How Redux works
- Wiring up Redux (all the gory details)
- 1. Use react-redux and wrap the Provider component around everything
- 2. Create the store
- 3. Make reducers to populate the store
- Wiring up Redux (just the necessary pieces with comments)
- CONNECTing Redux and React
- The absolute most basic use of Redux (no action creator, action, or dispatch just yet)
- Adding more reducers to our store
- Adding an action creator and action
- Adding the dispatcher
- Some review
- Adding the Router
- Getting more from Redux (clearing all the inventory)
- Redux Middleware (asynchronous action creators!)
- Before Hooks
- Hooks
- Supplemental Notes
Reference
create-react-app
(NPM): The NPM page largely just refers you on to several of the links listed below.create-react-app
(GitHub): See the latest additions/modifications tocreate-react-app
.- Glossary of React Terms: Get acquainted with all of the lingo and jargon surrounding React.
create-react-app
docs outline
create-react-app
docs homepage- Welcome
- Getting Started
- Development
- Styles and Assets
- Building your App
- Testing
- Back-End Integration
- Deployment
- Advanced Usage
- Support
React.Component
(docs reference in a nutshell)
See React.Component in the docs for all the gory details. Below is a modest attempt to provide just a nuts and bolts reference for ease of use (i.e., consult the actual docs for examples of everything below).
Each component has several "lifecycle methods" that you can override to run code at particular times in the process. You can use this lifecycle diagram as a cheat sheet. In the list below, commonly used lifecycle methods are marked as bold. The rest of them exist for relatively rare use cases.
- Mounting: These methods are called in the following order when an instance of a component is being created and inserted into the DOM:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
- Note: These methods are considered legacy and you should avoid them in new code:
- Updating: An update can be caused by changes to props or state. These methods are called in the following order when a component is being re-rendered:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate() These methods are considered legacy and you should avoid them in new code:
- Note: These methods are considered legacy and you should avoid them in new code:
- Unmounting: This method is called when a component is being removed from the DOM:
- Error Handling: These methods are called when there is an error during rendering, in a lifecycle method, or in the constructor of any child component.
- Other APIs: Each component also provides some other APIs:
- Class Properties
- Instance Properties
Conditional rendering cheatsheet
This article gives a very nice overview of conditional rendering in React. The author specifies the following waysof conditional rendering: if, if else, ternary, switch case, multiple conditional renderings in React, nested conditional rendering in React, conditional rendering with higher-order components, and finally if else components. Here's the actual cheatsheet:
- if
- most basic conditional rendering
- use to opt-out early from a rendering (guard pattern)
- cannot be used within return statement and JSX (except self invoking function)
- if-else
- use it rarely, because it's verbose
- instead, use ternary operator or logical && operator
- cannot be used inside return statement and JSX (except self invoking function)
- ternary operator
- use it instead of an if-else statement
- it can be used within JSX and return statement
- logical && operator
- use it when one side of the ternary operation would return null
- it can be used inside JSX and return statement
- switch case
- avoid using it, because it's too verbose
- instead, use enums
- cannot be used within JSX and return (except self invoking function)
- enums: multiple conditional renderings
- use it for conditional rendering based on multiple states
- perfect to map more than one condition
- nested conditional rendering
- avoid them for the sake of readability
- instead, split out components, use if statements, or use HOCs
- conditional rendering with higher-order components
- components can focus on their main purpose
- use HOC to shield away conditional rendering
- use multiple composable HOCs to shield away multiple conditional renderings
- external templating components: if else components
- avoid them and be comfortable with JSX and JS
React Router (docs reference)
The following are a few of the helpful links to get started with React Router:
- Official Documentation
- Quick Start Docs for React Router on the Web
- Philosophy of React Router
- GitHub Home for react-router
- NPM Home for react-router
- GitHub Home for react-router-dom
- NPM Home for react-router-dom
Now for the actual docs reference:
- Announcements
- Examples
- Guides
- API
Redux (docs reference)
Here is the homepage for Redux and here is the GitHub repo. And here are docs links for reference:
- Introduction
- Basic Tutorial
- Advanced Tutorial
- Recipes
- FAQ
- Style Guide
- Other
- API Reference
- Redux Toolkit
React-Redux (docs reference)
Here is the homepage for React Redux and here is the GitHub repo. And here are docs links for reference:
- Introduction
- Using React Redux
- API Reference
- Guides
Hooks API (docs reference)
React practical tutorial highlights (Tic-Tac-Toe)
JSX accommodates the use of any JavaScript expression
As noted in the docs:
JSX comes with the full power of JavaScript. You can put any JavaScript expressions within braces inside JSX. Each React element is a JavaScript object that you can store in a variable or pass around in your program.
Check out this post for a somewhat deep dive into expressions versus statements in JavaScript and this post for a more trimmed version.
Here's the gist:
Statement: A statement is a piece of code that can be executed and performs some kind of action. For example, if
is a statement:
let myStr;
if (myBool) {
myStr = 'Yes';
} else {
myStr = 'No';
}
One more example of a statement: a function declaration.
function twice(x) {
return x + x;
}
Expression: An expression is a piece of code that can be evaluated to produce a value. For example, the code between the parentheses is an expression:
let myStr = (myBool ? 'Yes' : 'No');
The operator ?
used between the parentheses is called the ternary operator. It is the "expression version" of the if
statement.Let's look at more examples of expressions. We enter expressions and the REPL evaluates them for us:
> 'ab' + 'cd'
'abcd'
> Number('123')
123
> true || false
true
What is allowed where? The current location within JavaScript source code determines which kind of syntactic constructs you are allowed to use:
- The body of a function must be a sequence of statements:
function max(x, y) {
if (x > y) {
return x;
} else {
return y;
}
}
- The arguments of a function call or a method call must be expressions:
console.log('ab' + 'cd', Number('123'));
However, expressions can be used as statements. Then they are called expression statements. The opposite is not true: when the context requires an expression, you can't use a statement.
The following code demonstrates that any expression bar()
can be either expression or statement – it depends on the context:
function f() {
console.log(bar()); // bar() is expression
bar(); // bar(); is (expression) statement
}
Expressions and statements in React: As noted above, there is a difference between expressions and statements in JavaScript. In the context of React, specifically as it concerns the usage of JSX, we are only permitted the use of JavaScript expressions within JSX. Per the discussion above, we could not use an if
statement in JSX in React. But we could use its "expression equivalent" in the ternary operator ?
, and we will see this being done very frequently.
React 101
Starter notes (NodeJS, create-react-app
, React docs, etc.)
As we will see, NodeJS is not necessary in order to use React, but you will be in for a world of pain if you don't use NodeJS. It will make your life so much easier. In particular, once we have made sufficient progress, we will be able to use the create-react-app
CLI and add on numerous other packages meant for React via Node. It's really the only way to go.
As noted at the top of this file, there are several React-specific docs to make development with React as painless as possible. Use the docs to your advantage!
What React is and why we need it
Before we get into the weeds as to how to use React, it would be a good idea to know what it is, where it comes from, what problems it has tried to solve, etc. The more background we can have on it before directly using it the better. Our background knowledge can inform our use.
In web development, 1995 was a monster year. Python (really 1991), Java, PHP, Ruby, Apache, and (prematurely) JavaScript were born. Netscape (now Mozilla) had Navigator (now Firefox) as the only real web browser at that point. They know Microsoft is coming to build their own browser so the CEO, Marc Andreessen, hires Brendan Eich to build a scripting language. That language is not meant to compete with the heavy lifters (i.e., Python, Java, PHP, Ruby, etc.) because those heavy lifters are already handling all of the back-end stuff. We want a scripting language that's accessible to amaeteurs and hobbyists and people who just want to dabble in basic programming. Why? Why not just make it a full-on language with inheritance and other features like those mentioned above? There are many reasons, but the main one is probably that in 1995 there's a grand total of about 23,000 websites. That may sound like a decent amount, but it's nothing at all--today we have billions of websites. So the web was in its absolute infancy in 1995. You're not going to be able to get heavy-lifter developers to migrate over to the JavaScript world since the ecosystem is so small at that point. And websites were mostly inert. Think Wikipedia. You open the page and that's it.
JavaScript came into the world to be the easy programming language while the heavy-lifters remained as they were. Fast forward to roughly 2005 and AJAX comes out. We have iOS and Android. jQuery comes out in 2006. And we hit 100 million websites. So in just 10 years, we went from around 23,000 websites to over 100 million. People are now carrying around full computers in their pockets that are able to run JavaScript. AJAX has revolutionized the web because internet connections are faster so you can send lots of little pieces of data. And in all of this jQuery was awesome. In some ways, jQuery kind of unified the DOM almost as a language across everything that used the DOM. And jQuery was awesome because it took us from web pages that used hundreds of lines of JavaScript and condensed them down to just a few lines. So jQuery was great, but the web was growing at a frenetic pace.
If we hop to around 2010, then the V8 engine has come out, Node.js has come out, and Angular is born (and BackBone). This is where things really start to change for JavaScript. At this point in history, Instagram, Netflix, and Twitch go on the web, and browsers have gone from needing to serve up a tiny number of pages in 1995 to now where you have major websites like Netflix, Walmart, Amazon, etc. All of these websites have gone from being little fun marketing sites in 2000 to being really important parts of the company. They're not just little applications but major software as a service or they're a major platform in being able to actually make money.
Angular is the first UI framework, and a UI framework is a framework that seeks to simplify your life. So first we had JavaScript files that became unmanageable. jQuery was a lifesaver and condensed the code we needed. The web continued to explode and then the jQuery files got out of hand. Angular sought to reign in the chaos. In 2013 enters React, and React is almost unanimously seen as a vast improvement over Angular 1 (not necessarily Angular as it is now). It is also a UI framework.
Basically, a UI framework is a whole bunch of JavaScript someone else has written to try and make your life easier. In what way does it make life easier? React seeks to answer the following question: "How can we modernize web development?" Because we can't have thousands of really long files of JavaScript. It becomes intractable. And if you're Facebook then you'll have a ton of those really monolithic files. So it answers this question in the following way:
- Modularize: It breaks up the application into a bunch of tiny little pieces. So once again the goal is to get back to the point where files are reasonably short and manageable. This is also good because it allows the files to be encapsulated. That is, we can follow some basic object-oriented programming principles and make our files easier to reuse. We can pass them around freely from one place to the next within our application (they manage their own data and all their methods are internal and so forth).
- Manages state effectively: This is something that Angular did not really do. We'll talk about this a lot more later, but now you have webpages that are changing constantly in a big way, and you need someone who is in charge of it, and it's not a good approach to just let the DOM be the source of truth anymore.
- Efficient: If you think about Facebook, then you can think about chats, notifications, messages, etc., all happening nearly instantly and all of those things are tiny DOM manipulations. And jQuery was computationally very very expensive in how it manipulated the DOM. React can do this in a very efficient way.
- Front-end/back-end separation of concerns: React completely separates the front-end from the back-end. There are a number of positive benefits about this, but just to name a few: You can have two separate teams (front-end people and back-end people). The front-end people can focus entirely on React and the back-end people can focus on their own thing. In the past, if you had the front-end and back-end teams tightly coupled, as was the case for many many years, then if one thing went down the other went down as well. If you wanted to change one you had to change the other. Separating them out makes everything much more modular, easier to manage and maintain, etc.
- Hardware increases: Your phone may have a stronger processor than, say, what's on your
T2.micro
on AWS. The hardware is just there now to where we want to offload as much as possible to the browser because the hardware that the user is running their browser on is outstanding, and for large-scale websites this can save so much processing power. - Declarative instead of imperative: We can get away from telling the computer exactly how to do something to simply say, "Hey, this is what I want you to do."
The super short version: What is React? React is a whole bunch of JavaScript that someone else wrote (mostly Facebook) that helps your development go from being boring, small, and unsophisticated to being big, exciting, professional, and organized. If you have a tiny website or project, then React is not what you need. React is made to build buildings and cities--it is not meant to make log cabins or tents. That's what the web was in the beginning. It is no longer that way. The web grew up. It is no longer boring with unsophisticated progammers. It is now professional programmers, large teams, huge companies (e.g., Amazon, Facebook, Google, etc.), and it allows you to make your UI (i.e., your front-end or your stuff inside the browser) really well.
First React program (and touching on JSX and Babel)
Without create-react-app
, it's not exactly extremely straightforward to get an application going with React. Remember that React is meant to build large websites. Here is what you get when professionally developing with React:
- React itself
- React DOM (when using React for the web)
- JSX (to make it easier to use declarative markup)
- ES6 (to take advantage of modern JS)
- Babel (to transpile modern JS and JSX into ES5 and lower JS all browsers can understand)
- Webpack (for module bundling)
- NodeJS (for too many things to count with
create-react-app
being first among many)
At the beginning, where we will start without using create-react-app
, we could certainly go the Express route (where we statically load one big folder and then load up the index.html
in whatever subfolder as we want), but we can just as well use the live server extension in VSCode to launch our local index.html
file instead. Whatever is most comfortable.
No matter what we do, we will need the following through the React CDN links as well as cdnjs for babel-standalone
:
- React:
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
- ReactDOM:
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
- Babel:
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
These links will give us access to React proper, ReactDOM (React can be used in contexts without a DOM, but we will be working in the context where there is a DOM), and Babel (babel-standalone
is a standalone build of Babel for use in non-NodeJS environments.) The skeleton for our index.html
file might look like the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>First React Program</title>
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
<!-- What goes in here now? -->
</body>
</html>
What goes in the body
? This is where the particulars of React will come into play. We will first have <div id="root"></div>
which is to serve as the "root" of our application. This is where our content will get dumped. Then below this div
we will want to drop a script
tag with type="text/babel"
so whatever we place within the script
tag can be processed properly:
<body>
<div id="root"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Sanity Check</h1>,
document.getElementById('root')
)
</script>
</body>
All in all we end up with the following most basic first React program:
Let's go through this in more detail than we did above:
1. React: This is React proper. It is React itself. You can see the non-minified version of React (it's only a couple thousand lines of code).
2. ReactDOM: This is ReactDOM. And we can see we make use of ReactDOM in our program by literally using ReactDOM
and calling the render
method on it. You can see the non-minified version of ReactDOM if you want (it's several thousand lines long). The heart of our program is lines 18-21 in the screenshot above, and this is where we make use of ReactDOM
, specifically the render
method. In our example we supplied render
with two arguments:
- Some HTML (our
h1
tag) - A container (a
div
in our case which we selected using basic JavaScript)
In fact,the docs note the following syntax for render
:
ReactDOM.render(element, container[, callback])
And we get the following basic description: "Render a React element into the DOM in the supplied container
[...]. If the optional callback is provided, then it will be executed after the component is rendered or updated."
3. Babel: This is Babel. And we can see we make use of Babel in our program via the script
tag with the attribute type="text/babel"
. You can see the non-minified version of Babel if you want (it's several thousand lines of code long). Check out babeljs.io to experiment and find out what it's really doing behind the scenes. As the website notes, Babel is a JavaScript compiler. We'll explore what this means further momentarily.
4. JSX: What is JSX? As always, the docs give us a clue, but the basic gist is this: JSX is a JavaScript (J) syntax (S) extension (X), hence the JSX name, and it is recommended to use with React so we can describe what the UI should look like (declarative vs. imperative). And JSX goes hand in hand with Babel which is also working with React.
What does all of this mean? Well, let's first explore what Babel does even without React first. It's a JavaScript compiler. It basically does two things:
- It makes the fancy new ES6+ JavaScript we use reverse-compatible so browsers can understand it (many browsers have not completely updated to support ES6+). As an example, something really simple like
() => 2
in ES6+ speak would be turned into
(function () {
return 2;
});
which any browser can understand.
- The second thing Babel will do (if we have
react
checked in the left sidebar presets when trying it out) is transpile our JSX into code React proper can work with (this is where React itself comes into play). So something like() => <h1>Sanity Check</h1>
gets transpiled by Babel into
(function () {
return React.createElement("h1", null, "Sanity Check");
});
As you can see, we are directly making use of React
; specifically, we are making use of the createElement
method and passing arguments to it. As the docs note, each JSX element is just syntactic sugar for calling React.createElement()
. How does createElement
work? The docs give us an example:
React.createElement(
type,
[props],
[...children]
)
The docs loosely note the following: This creates and returns a new React element of the given type. The type argument can be either a tag name string (such as 'div'
or 'span'
), a React component type (a class or a function), or a React fragment type. Code written with JSX will be converted (spoiler alert: this conversion is done using Babel!) to use React.createElement()
. You will not typically invoke React.createElement()
directly if you are using JSX. See React Without JSX to learn more.
Returning to our first program, our script
tag on line 17 with type="text/babel"
indicates that we want Babel to compile our code into something the browser can understand. If what we use in our script
is just JavaScript, then it will simply make the JavaScript ES6+ reverse-compatible. But if it's JSX, then it will transpile our code into something React knows how to handle. In our own case, Babel took () => <h1>Sanity Check</h1>
and turned this into
(function () {
return React.createElement("h1", null, "Sanity Check");
});
From above, we see that the type
we gave it was an h1
, null
for [props]
(we'll get to props and all that good stuff soon enough, but right now you can think of props
as basically attributes on a normal HTML element), and "Sanity Check"
for [...children]
. Of course, we may have cases where we have a lot more than a single child. Consider something still rather basic but that could be a nightmare to deal with without Babel:
() => (
<div>
<h1>Heading</h1>
<h2>First Subsection</h2>
<p>
Little paragraph in subsection and we may link to <a href="google.com">Google</a> or something like that.
</p>
<p>Another <span>little</span> paragraph</p>
</div>
)
Babel will turn this into the following:
(function () {
return React.createElement("div", null, React.createElement("h1", null, "Heading"), React.createElement("h2", null, "First Subsection"), React.createElement("p", null, "Little paragraph in subsection and we may link to ", React.createElement("a", {
href: "google.com"
}, "Google"), " or something like that."), React.createElement("p", null, "Another ", React.createElement("span", null, "little"), " paragraph"));
});
You can easily see how children of the single div
can have multiple children themselves and things can quickly spiral out of control.
More JSX and Babel
Facebook made JSX and you can see the GitHub repository for JSX if you're really into that. As they note in the description for the repository: The JSX specification is a XML-like syntax extension to ECMAscript. JSX is made basically for React. Because using React without JSX quickly becomes impossible as the end of the note above started to hint towards but which we will quickly see even more soon.
Returning to the basics, we see that Babel will take something like
<div id="root">I love React!</div>
and turn it into
React.createElement("div", {
id: "root"
}, "I love React!");
So what looks like HTML to us is not that at all when Babel is looking at it (especially in the context of using React). It's pure JavaScript. We could maybe add a class to our HTML element like so:
<div id="root" class="container">I love React!</div>
But we cannot do this! Why? Because class
is a keyword in JavaScript. In fact, Babel will take a silly class
like
class Car {
constructor(color, mileage) {
this.color = color;
this.mileage = mileage;
}
showMileage() {
return this.milage;
}
}
and compile it into
var Car = function () {
function Car(color, mileage) {
_classCallCheck(this, Car);
this.color = color;
this.mileage = mileage;
}
_createClass(Car, [{
key: "showMileage",
value: function showMileage() {
return this.milage;
}
}]);
return Car;
}();
Hence, in React, we do not use class
for a class
attribute we might normally put on an HTML element. Instead, we use className
:
<div id="root" className="container">I love React!</div>
And Babel turns this into
React.createElement("div", {
id: "root",
className: "container"
}, "I love React!");
So every attribute that we add in JSX will be added as a prop(erty) in the second argument to React.createElement
. Another "gotcha" to remember in React is we always need to close our elements even if they may be conventionally self-closing. For example, we need <br />
not <br>
, <img />
not <img>
, etc.
We can create a more interesting React program (though still tiny in the grand scheme of things) in the following manner:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>First React Program</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const markup = <div className="row">
<div className="col s2">
<div className="card hoverable small">
<div className="card-image">
<img src="https://picsum.photos/400/400/" />
</div>
<div className="card-content">
<p>React From the Beginning</p>
<p>Robert Bunch</p>
</div>
<div className="card-action">
<a href="#">$9.99</a>
</div>
</div>
</div>
</div>
ReactDOM.render(
markup,
document.getElementById('root')
)
</script>
</body>
</html>
Not the addition of the materialize link: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
. Of course, the real addition is the markup
variable. The entire thing looks like HTML, but it isn't. It's JSX. If we throw everything in the markup
variable into Babel, we will see what is really being done underneath the hood:
React.createElement("div", {
className: "row"
}, React.createElement("div", {
className: "col s2"
}, React.createElement("div", {
className: "card hoverable small"
}, React.createElement("div", {
className: "card-image"
}, React.createElement("img", {
src: "https://picsum.photos/400/400/"
})), React.createElement("div", {
className: "card-content"
}, React.createElement("p", null, "React From the Beginning"), React.createElement("p", null, "Robert Bunch")), React.createElement("div", {
className: "card-action"
}, React.createElement("a", {
href: "#"
}, "$9.99")))));
What a mess! Imagine having to do this every single time instead of just using JSX. The real power of all of this isn't just the fact that Babel does a bunch of compiling/transpiling underneath the hood. The power comes from how modular everything can be when we note that all of this is just JavaScript. So we can make variables and the like, perform computations, etc., and place the results in our JSX dynamically. The result can eventually be a bunch of dynamically rendered HTML. So how do we insert variables and the like into JSX?
A decent way of thinking about JSX is that it is in "HTML mode" by default, where nothing is dynamic. But we can use curly braces { javascrpt-mode }
to enter "JavaScript mode." So when Babel gets to a curly brace it expects whatever is inside to be an expression in JavaScript that can be evaluated. The expression is evaluated and placed and when the closing curly brace is encountered, HTML mode resumes. So when Babel encounters something like {title}
it knows you mean the variable title
as opposed to the string 'title'
. We can run almost anything we want inside of the curly braces except for a full-blown statement like an if...else
statement, a for
loop, etc. But this is also where the power of the ternary ?
in JavaScript comes into play. We can make a more dynamic HTML file like the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>First React Program</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const title = 'React From the Beginning';
const name = 'Robert Bunch';
// const saleOn = false;
function saleOn() {
return true;
}
const markup = <div className="row">
<div className="col s2">
<div className="card hoverable small">
<div className="card-image">
<img src="https://picsum.photos/400/400/" />
</div>
<div className="card-content">
<p>{title}</p>
<p>{name}</p>
</div>
<div className="card-action">
<a href="#">${saleOn() ? 9.99 : 59.99}</a>
</div>
</div>
</div>
</div>
ReactDOM.render(
markup,
document.getElementById('root')
)
</script>
</body>
</html>
If we dumped everything within the script
tags in the body
, we would end up with the following via Babel:
var title = 'React From the Beginning';
var name = 'Robert Bunch';
function saleOn() {
return true;
}
var markup = React.createElement("div", {
className: "row"
}, React.createElement("div", {
className: "col s2"
}, React.createElement("div", {
className: "card hoverable small"
}, React.createElement("div", {
className: "card-image"
}, React.createElement("img", {
src: "https://picsum.photos/400/400/"
})), React.createElement("div", {
className: "card-content"
}, React.createElement("p", null, title), React.createElement("p", null, name)), React.createElement("div", {
className: "card-action"
}, React.createElement("a", {
href: "#"
}, "$", saleOn() ? 9.99 : 59.99)))));
ReactDOM.render(markup, document.getElementById('root'));
Yikes! Definitely use JSX. Apart from the ease, JSX also prevents injection attacks. So it is safe to embed user input in JSX.
Understanding React elements as opposed to DOM elements
What is a React element? The docs note that an element describes what you want to see on the screen:
const element = <h1>Hello, world</h1>;
And that unlike browser DOM elements, React elements are plain objects and are cheap to create. React DOM takes care of updating the DOM to match the React elements. To get a sense of how much cheaper it is to create a React element than a DOM element, consider the following basic HTML file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sample React Element</title>
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const element = <h1 id="sample-react-element">Hello, world</h1>;
console.log('This is a React element:');
console.dir(element);
console.log('============================================================');
console.log('========== React element above; DOM element below ==========');
console.log('============================================================');
ReactDOM.render(
element,
document.getElementById('root')
)
const sampleElement = document.getElementById('sample-react-element');
console.log('This is a DOM element:');
console.dir(sampleElement);
</script>
</body>
</html>
What's happening here? First, we create a React element with the JSX const element = <h1 id="sample-react-element">Hello, world</h1>;
which is converted to the following using Babel:
var element = React.createElement("h1", {
id: "sample-react-element"
}, "Hello, world");
Before this React element is created in the DOM, we display an interactive list of the properties of this object in the console via console.dir
. After React has updated the DOM, by transforming our element
into an actual DOM element by placing it in the DOM, we select our "now actual DOM element" by using basic JavaScript: const sampleElement = document.getElementById('sample-react-element');
, and then we display an interactive list of the properties of our "now actual DOM element". The beginning of the output will look like this:
To fully appreciate the difference, watch what happens when you view and expand some of the React element's properties as opposed to the actual DOM element's properties:
Talk about the DOM element not being cheap! That object is huge with all sorts of properties on it. The React object is just a plain old JavaScript object (POJO) with only a few properties. It's when React injects it into the actual DOM that it becomes a real DOM element with all of the crazy extensive properties one might expect of a DOM element.
ReactDOM.render()
and the virtual DOM
ReactDOM.render
takes two arguments (really three, with the third one being optional as a callback):
- What we want to render (i.e., a React element which is probably some JSX)
- Where we want to render it (this will be an actual DOM element, something that already exsists in our markup as of page load)
Let's return to our example from earlier where we were dropping some variables in our JSX:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>First React Program</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const title = 'React From the Beginning';
const name = 'Robert Bunch';
// const saleOn = false;
function saleOn() {
return true;
}
const markup = <div className="row">
<div className="col s2">
<div className="card hoverable small">
<div className="card-image">
<img src="https://picsum.photos/400/400/" />
</div>
<div className="card-content">
<p>{title}</p>
<p>{name}</p>
</div>
<div className="card-action">
<a href="#">${saleOn() ? 9.99 : 59.99}</a>
</div>
</div>
</div>
</div>
ReactDOM.render(
markup,
document.getElementById('root')
)
</script>
</body>
</html>
We can now start to see where the full power of React comes into play: On the first go around, React has to build out the entire DOM (of course, this is quite expensive, as we observed through the note above when we looked at actual DOM elements). BUT from here on out (i.e., after React has built and rendered the DOM on the initial page load), any time something inside of <div id="root"></div>
changes, instead of it being an actual DOM element, it's a React element, and React in the background keeps track of the old as well of the new. So it compares the two objects. Again, they're not DOM objects. They're React elements which are regular JavaScript objects which are cheap and small. It compares the two and sees what's actually different.
Here's a clearer verbal description of what React does behind the scenes: When ReactDOM.render
is first called, React builds the virtual DOM which consists only of React elements (i.e., plain old JavaScript objects or POJOs that are cheap and small). Then React builds out the actual DOM (which consists of all of the very expensive objects that have tons of properties and prototypes) from the virtual DOM it just created. Now suppose something happens in your application where something is supposed to be updated on the page (e.g., someone types something in). Instead of just automatically updating the DOM, what React will do is create a completely new virtual DOM and compare this new virtual DOM with the old virtual DOM, comparing everything in both virtual DOMs. When it comes across any differences in the new DOM compared to the old DOM, instead of updating the entire DOM tree (which is very common and incredibly expensive computationally), React will only update the thing(s) that changed. The next time something happens that is supposed to update the page will result in React again making a new virtual DOM, comparing it to the old virtual DOM, and updating the actual DOM with any differences that occurred, and so on and so forth:
So the speed is not only in that you are changing/updating only what needs to be changed/updated but also that you are comparing regular JavaScript objects with regular JavaScript objects instead of DOM elements with DOM elements. That is what makes all of this worth it and what makes React so fast.
Components
So far we have rendered things two ways via ReactDOM.render
. First, we put our JSX directly in the render
:
...
<script type="text/babel">
ReactDOM.render(
<h1>Sanity Check</h1>,
document.getElementById('root')
)
</script>
...
Second, we assigned our JSX to a variable and then passed that variable to render
:
...
<script type="text/babel">
const title = 'React From the Beginning';
const name = 'Robert Bunch';
function saleOn() {
return true;
}
const markup = <div className="row">
<div className="col s2">
<div className="card hoverable small">
<div className="card-image">
<img src="https://picsum.photos/400/400/" />
</div>
<div className="card-content">
<p>{title}</p>
<p>{name}</p>
</div>
<div className="card-action">
<a href="#">${saleOn() ? 9.99 : 59.99}</a>
</div>
</div>
</div>
</div>
ReactDOM.render(
markup,
document.getElementById('root')
)
</script>
...
As fate would have it, neither of these ways is the preferred React way. Of course these ways work, but React is meant to be component-based. Everything in React is meant to be a component. It's just a bunch of little Lego pieces or modules pieced together to actually formulate your UI.
How will this actually work? We will create a Card
component by creating a Card.js
file with the following as its contents:
// Card.js
function Card() {
const title = 'React From the Beginning';
const name = 'Robert Bunch';
function saleOn() {
return true;
}
return (
<div className="row">
<div className="col s2">
<div className="card hoverable small">
<div className="card-image">
<img src="https://picsum.photos/400/400/" />
</div>
<div className="card-content">
<p>{title}</p>
<p>{name}</p>
</div>
<div className="card-action">
<a href="#">${saleOn() ? 9.99 : 59.99}</a>
</div>
</div>
</div>
</div>
)
}
It is worth noting here that everything that happens above the return
in the Card
function is just plain JavaScript. That is, structurally, we will have the following for components:
function Card(props) {
// a bunch of pure JavaScript
return (
// a bunch of JSX to be processed by Babel
)
}
Now we can create our index.html
to use this Card
component in the following manner:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>First Component</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="./Card.js" type="text/babel"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
ReactDOM.render(
<Card />,
document.getElementById('root')
)
</script>
</body>
</html>
Two things to note right out of the gate:
type="text/babel"
: This will not work:<script src="./Card.js"></script>
. It is imperative that this script have an attribute oftype="text/babel"
so Babel will know to process it. So we need<script src="./Card.js" type="text/babel"></script>
, as indicated above.<Card />
: This is essentially going to invoke theCard
function inCard.js
and will get the return value, which is a bunch of JSX, from theCard
function living inCard.js
. If you use a different name for your component such as<Different />
, then this will not work. We need<ComponentName />
to match the functionComponentName
in whatever file theComponentName
function is located in. Often a file's name will reflect what component lives within it, but we could just as well have namedCard.js
asHonk.js
and the function insideCraziness
and then we would need to have<script src="./Honk.js" type="text/babel"></script>
and use<Craziness />
. The point is that<ComponentName />
needs to match the function name returning a component in whatever file that component lives in. As stated previously, your file name will generally be named to reflect what component lives within it and that component will be used as such within your application.
Recapping, our script
tag must have type="text/babel"
because what the script
tag points to (e.g., the Card
function) needs to be processed by Babel. That way, when the Card
function is invoked within our code by <Card />
, what we are getting is processed JSX by Babel instead of just a bunch of gibberish that JavaScript won't understand. Hence, something like
ReactDOM.render(
<Card />,
document.getElementById('root')
)
will actually make sense because <Card />
is effectively being replaced by what the Card
function returns from Card.js
(a bunch of JSX that, thanks to type="text/babel"
, has been processed by Babel into JavaScript the browser will understand).
Of course, at this point, we're not so much improving how anything looks, but we are getting closer and closer to how React is meant or intended to function. Now, <Card />
is a component, and this component looks like an HTML tag, but it always starts with an uppercase letter. The reason for that is because when React is parsing through our code, if it runs into lowercase stuff, it's going to assume it's either an HTML tag or it's an XML tag. If, however, it seens an uppercase letter, it will assume it's actually a component. As the docs note: "Always start component names with a capital letter. React treats components starting with lowercase letters as DOM tags. For example, <div />
represents an HTML div
tag, but <Welcome />
represents a component and requires Welcome
to be in scope. To learn more about the reasoning behind this convention, please read JSX In Depth."
Components are not just the backbone of React but really React in its entirety. As we get to do cooler and cooler things with React, we'll quickly start to notice that the entire UI or entire front-end is really just a whole bunch of components. We're always going to start with one (probably something like <App />
) that has a whole bunch of components in it where the components inside have components in them, etc. It really is like a bunch of Legos that fit together to make up something awesome.
Props
Anytime you have a component in React you have the option of adding attributes to that component. Before we just had <Card />
but we could also have something like <Card name="Daniel Farlow" job="Developer"/>
. Of course, attributes in HTML are typically things like id
, class
, width
, etc., but now we are making up our own "attributes". What will happen to the attributes we put on our component is that when the component is called (i.e., think previously about how <Card />
invoked or called the Card
function in Card.js
that returned a bunch of processed JSX), the component is handed an argument which is always called props
(since it's a local variable you can call it whatever you want, but convention is to call it props
so you should always do that). So in our Card.js
file our Card
function really should have function Card(props) { ... }
.
Suppose our Card.js
file looked like this:
function Card(props) {
console.log('The props: ', props);
const title = 'React From the Beginning';
const name = 'Robert Bunch';
function saleOn() {
return true;
}
return (
<div className="row">
<div className="col s2">
<div className="card hoverable small">
<div className="card-image">
<img src="https://picsum.photos/400/400/" />
</div>
<div className="card-content">
<p>{title}</p>
<p>{name}</p>
</div>
<div className="card-action">
<a href="#">${saleOn() ? 9.99 : 59.99}</a>
</div>
</div>
</div>
</div>
)
}
And our index.html
file was this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>First Component</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="./Card.js" type="text/babel"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
ReactDOM.render(
<Card name="Daniel Farlow" job="Developer"/>,
document.getElementById('root')
)
</script>
</body>
</html>
Then upon firing up everything and looking in the console, we would see the following:
Note that props
is just an object and that every attribute that you give your component is sent over as props
. The props
naming convention is sensible and conventional because props
is actually a JavaScript object that will have a property for every attribute that you set on your component. The attribute name itself will be a key on the props
object while what you assign to the named attribute will be the key's value. So something like <Card name="Daniel Farlow" job="Developer"/>
results in props
looking like { name: 'Daniel Farlow', job: 'Developer' }
. The upshot of all of this is that you can use props
within your components to make the components dynamic and reusable.
For example, if our HTML looks like this:
...
<script type="text/babel">
ReactDOM.render(
<Card title="React From the Beginning" name="Robert Bunch"/>,
document.getElementById('root')
)
</script>
...
Then our Card.js
file can look like this:
function Card(props) {
const { title: courseTitle, name: courseInstructor } = props;
function saleOn() {
return true;
}
return (
<div className="row">
<div className="col s2">
<div className="card hoverable small">
<div className="card-image">
<img src="https://picsum.photos/400/400/" />
</div>
<div className="card-content">
<p>{courseTitle}</p>
<p>{courseInstructor}</p>
</div>
<div className="card-action">
<a href="#">${saleOn() ? 9.99 : 59.99}</a>
</div>
</div>
</div>
</div>
)
}
How awesome is that! What does this accomplish? Essentially, it lets us create a single Card
component that could be used numerous times in different contexts:
<script type="text/babel">
ReactDOM.render(
<React.Fragment>
<Card title="React From the Beginning" name="Robert Bunch"/>
<Card title="Apache Kafka Series" name="Stephane Maarek"/>
</React.Fragment>,
document.getElementById('root')
)
</script>
Two things to note here:
React.Fragment
: See the docs for more on fragments. The basic idea is that fragments let you group a list of children without adding extra nodes to the DOM. There is also a shorter way to use them with<>
and</>
, but note that this does not support the use of keys or attributes. You can see more about keyed fragments and why that might be a good idea (think of creating a description list).- We just created two
Card
components very easily by only passing what was different to the cards. That is, we want the cards to look the same but obviously have only the content that's relevant or specific to them.
This is the power of components! The fact that we can send data down to them makes it almost like a function where instead of passing arguments we are passing props
. One thing to note is that props are immutable. They are managed by the parent and never managed by the component itself (of course, you could change this behavior by hijacking things with JavaScript within your component, but that's a big no-no and defeats the whole point of components). All components with their props are meant to be pure; that is, given the same props, the component should always look the same. So you never manually mutate or change props. They're meant to be pure.
We can even use the data
object we have in data.js
:
const data = [
{
course: "React From the Beginning",
instructor: "Robert Bunch"
},
{
course: "Apache Kafka Series",
instructor: "Stephane Maarek"
},
{
course: "Music Production in Logic Pro X",
instructor: "Tomas George"
},
{
course: "Unity Game Development",
instructor: "Jonathan Weinberger"
}
]
And we can use this via <script src="./data.js"></script>
in our index.html
file to make several cards dynamically:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>First Component</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="./Card.js" type="text/babel"></script>
<script src="./data.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
ReactDOM.render(
<div className="row">
<Card title={data[0].course} name={data[0].instructor}/>
<Card title={data[1].course} name={data[1].instructor}/>
<Card title={data[2].course} name={data[2].instructor}/>
<Card title={data[3].course} name={data[3].instructor}/>
</div>,
document.getElementById('root')
)
</script>
</body>
</html>
Of course, we will get to the point soon where we can easily loop through everything and create it even more dynamically.
Multiple components in an array (and unique "key" prop
warning)
One thing that is super cool in React is that you can build your components in an iterative fashion. Consider the following code:
let cards = data.map(courseData => (
<Card data={courseData}/>
))
console.log(cards)
ReactDOM.render(
<div className="row">
{cards}
</div>,
document.getElementById('root')
)
If we do something like this, then currently we will get a warning like the following:
What this means is that cards
is an array of React elements, the type is a function, and it has a key
with a value of null
(also has ref
with value of null
, etc.). The problem is that you can basically think of this as a linked list in the context of the virtual DOM. React wants these elements in the array to have keys so that if the state of the application changes React knows which thing changes so that it doesn't have to update the entire thing. If you don't provide a key, then it won't know what it actually needs to change and will have to rebuild the whole array which is expensive which is against the whole ethos of React. The gist: Whenever you build an array of React elements, if you give a key, then React will be a lot faster. When using map
to build an array, you could just make each key
the value of the index
(this practice is a last resort though--the key
should ideally be something unique to each item like an ID). Hence, our code becomes:
let cards = data.map((courseData, index) => (
<Card data={courseData} key={index}/>
))
console.log(cards)
ReactDOM.render(
<div className="row">
{cards}
</div>,
document.getElementById('root')
)
And the warning goes away.
Components as classes
Up until now the only type of component we have made is a regular JavaScript function. For example:
function Card(props) { ... }
This is a great and common way to make functions that are simply presentational or stateless. It means they don't need to make any decisions. But there is another very important way to make components. And this way is with classes. (The introduction of Hooks has made it possible to use stateful components with functions, and we'll get to all of that much later.)
The way we have kind of done things before is like the following:
class Card {
return (
<h1>Sanity Check</h1>
)
}
But this is not okay anymore because inside of a class
in JavaScript the only thing you are allowed to define are properties and methods. We can't just run JavaScript code. We need to put the JSX that we want to return inside a method in order to properly adhere to how class
es work in JavaScript. What method should we use? It turns out there is a convention/mandate from React and the docs spell this out in more detail: "The only method you must define in a React.Component
subclass is called render()
. All the other methods are optional." Seems important! If you use a class then, you must have a render
method:
class Card {
render() {
return (
<h1>Sanity Check</h1>
)
}
}
It turns out the code above is still not good enough. It's not enough to just define the component using a class
. In order to get all the goodness of a React component, we actually need too extend the React component (i.e., the Card
class needs to first inherit everything from the React.Component
class and then extend it by using our own methods, properties, and the like):
class Card extends React.Component {
render() {
return (
<h1>Sanity Check</h1>
)
}
}
What this does is it makes our class, Card
, a subclass of React.Component
. So React.Component
has a bunch of stuff we are going to "inherit". All the cool stuff that belongs with being a React component can now be used as part of our Card
component. A couple things to note here. This
function Card(props) {
return (
<h1>Sanity Check</h1>
)
}
is exactly the same as
class Card extends React.Component {
render() {
return (
<h1>Sanity Check</h1>
)
}
}
right now. They're functionally the same although syntactically different. A class gives you all sorts of power that you do not have with a function. (At least this used to be the case--this is different now with the advent of Hooks.) From the docs: "React lets you define components as classes or functions. Components defined as classes currently provide more features which are described in detail on this page. To define a React component class, you need to extend React.Component
." So more often than not you will be making components as classes instead of functions. Again, if you make a class component, then it must have a render
method; otherwise, the component is totally useless. It's the only method that a class must have in React. So don't forget to add it! And we have to use extends React.Component
for class components; otherwise, the class is just a regular garden-variety class. And sometimes you'll make classes that you don't extend (e.g., utility classes that don't have anything to do with React just to clean up your JavaScript).
If you've done much with classes before in JavaScript, then you will know that the constructor
is another method available to classes (every class
in JavaScript gets this method whether or not the class in question is a React component or just a garden-variety class). The constructor
method will run when an instance of the class
(or in our case a React component instance) is created. This gives us the ability to initialize instance variables and initialize state (the notion of state in React is a very important one we will get to momentarily).
Per the docs on constructor(props)
: "If you don't initialize state and you don't bind methods, you don't need to implement a constructor for your React component. The constructor for a React component is called before it is mounted. When implementing the constructor for a React.Component
subclass, you should call super(props)
[within constructor(props)
] before any other statement. Otherwise, this.props
will be undefined in the constructor, which can lead to bugs. Typically, in React constructors are only used for two purposes: Initializing local state by assigning an object to this.state
and binding event handler methods to an instance."
The upshot of all of this is that, in order for us to use state, we need to call the super(props)
method within our constructor
:
class Card extends React.Component {
constructor(props) {
super(props);
// more stuff to come
}
render() {
return (
<h1>Sanity Check</h1>
)
}
}
The constructor
will run every time a new Card
is created. And every time the constructor
is called the first thing that should get called is super
, which is the constructor
method of the parent class. So every time we create a new Card
our own constructor
will run, but we will first call super
in order to run the constructor
of the parent/super class (i.e., React.Component
). As the docs noted, the constructor is really only necessary if we are trying to initialize state and/or trying to bind methods to a class instance.
One last thing to note right off the bat is how props
are accessed within classes. For a regular JavaScript class, how do you refer to properties of the class? With the this
keyword:
class Dog {
constructor(name, friends, legs) {
this.name = name;
this.friends = friends;
this.legs = 4;
}
sayName() {
return this.name;
}
}
In the silly example above, how did we refer to the name
property of the class instance within the sayName
method? Not by name
but by this.name
. Why? Because, as MDN notes (under the "Instance properties") subsection, instance properties must be defined inside of class methods (they give the following example with the simple Rectangle
class):
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
The name
instance property for Dog
is defined inside of the constructor
class method. For example, something like let myDog = new Dog('Archie', ['Felix', 'Bruno']);
will result in the myDog
instance of Dog
having instance properties of
name
:'Archie'
friends
:['Felix', 'Bruno']
legs
:4
And we access these instance properties in methods within the class using the this
keyword. So something like myDog.sayName()
results in the sayName
method of the Dog
class being called on the Dog
instance of myDog
. Hence, this
points to myDog
in this case so myDog.sayName()
would result in 'Archie'
.
In the exact same manner (albeit more complicated fashion), in a React class component, we do not refer to props
within the render
method simply as props
but by this.props
. Where do our class instance properties get defined? When we create the component in question and pass props to them; for example, if PlayingCard
were a class component, then something like <PlayingCard value="12" suit="Spades" />
would result in implicitly having this.props.value = "12"
and this.props.suit = "Spades"
underneath the hood:
class PlayingCard extends React.Component {
constructor(props) {
super(props);
console.log(props); // { value: "12", suit: "Spades" }
}
render() {
const cardValueMap = {
'1': 'ace',
'2': 'two',
'3': 'three',
'4': 'four',
'5': 'five',
'6': 'six',
'7': 'seven',
'8': 'eight',
'9': 'nine',
'10': 'ten',
'11': 'jack',
'12': 'queen',
'13': 'king'
}
const { suit, value } = this.props;
const translatedValue = cardValueMap[value];
const phrase = translatedValue.slice(0,1).toUpperCase()
+ translatedValue.slice(1)
+ ' of ' + suit;
return(
<p>This card is a {phrase}</p>
)
}
}
If we throw <script type="text/babel" src="PlayingCard.js"></script>
into our index.html
and <PlayingCard value="12" suit="Spades"/>
into our app.js
, we will get This card is a Queen of Spades
placed in the DOM.
Returning to our original Card
example, the once functional component
function Card(props) {
console.log(props)
const { course: courseTitle, instructor: courseInstructor, image: courseImage } = props.data;
return (
<div className="col s2">
<div className="card hoverable small">
<div className="card-image">
<img src={courseImage} />
</div>
<div className="card-content">
<p>{courseTitle}</p>
<p>{courseInstructor}</p>
</div>
<div className="card-action">
<a href="#">$9.99</a>
</div>
</div>
</div>
)
}
can become the functionally equivalent (albeit imbued with many more potential powers now) class component:
class Card extends React.Component {
constructor(props) {
super(props);
}
render() {
const { course: courseTitle, instructor: courseInstructor, image: courseImage } = this.props.data;
return (
<div className="col s2">
<div className="card hoverable small">
<div className="card-image">
<img src={courseImage} />
</div>
<div className="card-content">
<p>{courseTitle}</p>
<p>{courseInstructor}</p>
</div>
<div className="card-action">
<a href="#">$9.99</a>
</div>
</div>
</div>
)
}
}
Worth noting is that for functional components we could run any plain JavaScript we wanted before we return
ed the JSX we wanted to. Similarly, in a class component, within the render method, we can run any plain JavaScript we want before we return
the JSX we want, as indicated above and also in the PlayingCard
example.
Using classes is actually rather helpful in general when thinking about your React components because it almost enforces the concept of encapsulation on the developer. That is, a given object should not only contain all of its own data but should also contain all the methods that change and effect that data. So we are going to try to make our components as self-sufficient as possible. They'll carry their data around with them, and they'll also carry their render
around with them. They'll carry other methods around with them as well so that ideally you'll be able to move these components across applications or parts of applications and it should be as seamless as possible.
React Essentials: Recap
What Is React and Why Do We Need It?
React is a bunch of JavaScript that someone else wrote that makes it easier to do front-end web development. React modernizes front-end web development by doing the following:
- Making the front-end modular via components (components are encapsulated, meaning they manage themselves)
- Making it much easier to maintain across teams and even years
- Simplifying state changes in an application
- Getting front-end applications to run very, very fast
- Separating front-end from back-end
React in Its Simplest Form
Recall our first React program:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>First React Program</title>
<!-- This is React proper -->
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<!-- This is ReactDOM -->
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<!-- This is Babel -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Sanity Check</h1>, // <-- This is JSX
document.getElementById('root')
)
</script>
</body>
</html>
Here's the breakdown from the comments above:
- React: Everything we make is a "React" element, not a DOM element.
- ReactDOM:
ReactDOM
uses itsrender
method to take our React elements and inject them into the actual DOM (on the webpage). - JSX: Allows us to commingle HTML and JavaScript. This saves us from having to write TONS of JavaScript.
- Babel: Converts our JSX into something the browser can read.
More explicitly, React allows us to create React elements via React.createElement
. ReactDOM allows us to use ReactDOM
's render
method to get React elements into the actual DOM from the React's virtual DOM (i.e., onto the actual webpage). JSX, via Babel, makes it possible for us to write
<h1 className='root'>Sanity Check</h1>
instead of writing
React.createElement("h1", { className: "root" }, "Sanity Check");
as you can see through an interactive session on babeljs.io. We are also allowed to embed JavaScript expressions in JSX using the so-called wax-on/wax-off technique: { <-- wax-on | expression here | wax-off --> }
:
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
ReactDOM and the virtual DOM
React keeps track of all React elements in a "virutal DOM" object. Whenever something changes, React builds a new "virtual DOM" object and ReactDOM compares them:
Because React elements are just plain JavaScript objects, React is very, very fast with its ability to update only what is necessary in the real DOM (where objects are far more weighted and computationally expensive to create); that is, ReactDOM updates only the part of the DOM that needs to change.
Component Basics
- Components are the backbone of React.
- They are little pieces that make up the entire UI.
- They always start with a capital letter.
- They must close along with all other React elements (i.e., neither
<br>
nor<Card something="else">
will do; instead, we must use<br />
and<Card something="else" />
, as an example). - Components look like HTML tags in JSX (but uppercase):
<Card />
. - Components always return some HTML so ReactDOM has something to put on the page. Remember that React is a front-end UI so every component we make has to have something for the UI in it. So every component has to return some HTML (technically JSX is what gets returned, which is processed by Babel and subsequently React uses what was processed to create React elements which subsequently get created as actual DOM elements via ReactDOM).
- Components can be pure functions (stateless or simple).
- Components can be classes (stateful or complex).
- Note: Hooks make it possible to use stateful functions but we'll get to that later.
Prop Basics
- Components are a lot like JavaScript functions.
- They can be rendered as many times as needed.
- In order to change when they render, components can be sent any data you wish (like an argument in a function). The data that gets passed to a component is called
props
. - A prop is anything inside a Component call after the Component name and looks like an HTML attribute:
- A prop's value comes after the
=
, just like an HTML attribute. - A prop value can be accessed inside the component.
props
is always an object. - The
props
object will have a property for each prop that was passed when the component was created. - The value of the property will be the value of that prop:
Components in an Array
- React allows us to put components in an array.
- JSX can unpack that array.
- We typically use
.map()
to build the array of components. map()
builds a new array and expects a return value.
Components as Classes
- Aside from regular JavaScript functions, components can also be made as classes.
- Classes themselves do not return JSX; they have a
render
method that returns JSX. - Classes always extend
React.Component
(unless you have a utility class you are using that has nothing to do with React) so that your custom class, which viaextends
is a subclass of React'sComponent
superclass, inherits all the goodness that comes from being a React component. When initializing state or binding methods,constructor(props) {super(props); ... }
will need to be used at the top of your class. - Props work the same way in a class as they do in a function except we refer to the props in a class by
this.props
instead of simplyprops
. - Classes (currently) come with more powers than plain JavaScript functions, as noted in the docs.
Breaking Down Components into Smaller Parts (i.e., subcomponents)
- Components can contain other components.
- Think of it like the DOM:
- A
div
often lives inside anotherdiv
. - A
<City />
can live inside a<CitiesContainer />
.
- A
JavaScript Inside Components
Recall that you can embed JavaScript expressions in JSX by putting any valid JavaScript expression (simply: any valid unit of code that resolves to a value) inside curly braces in JSX: { JavaScriptExpression }
. Importantly, we cannot use non-expression JavaScript code inside of JSX. So a question becomes: Where can you perform "normal" or "heavy duty" JavaScript within components before returning your desired JSX? This depends on whether or not you have a function or a class:
- Function: In functional components, regular JavaScript can be used before you
return
your JSX:
function FunctionComponent(props) {
// Do all non-expression or "heavy-lifting"
// JavaScript stuff here before returning JSX below
return (
// JSX
)
}
- Class: In class components, regular JavaScript can be used before you
return
your JSX in therender
method (as with regular JavaScriptclass
es, plain JavaScript cannot be left outside of a method, whether it be theconstructor
method or a custom method):
class ClassComponent extends React.Component {
constructor(props) {
super(props);
// initialize state; bind methods
}
// JavaScript CANNOT go here
render() {
// Do all non-expression or "heavy-lifting"
// JavaScript stuff here before returning JSX below
return(
// JSX
)
}
}
State and Events (and create-react-app
)
npx
vs. npm
in regards to create-react-app
(use npx
!)
This answer on Stack Overflow does an excellent job of making clear what npm
and npx
actually are and why we might want to use one over the other. Some key takeaways:
npm
: Manages packages but doesn't make life easy executing any.npx
: A tool for executing Node packages.npx
comes bundled withnpm
version5.2+
.The major advantage of
npx
is the ability to execute a package which wasn't previously installed:$ npx create-react-app my-app
The above example will generate a
react
app boilerplate within the path the command had run in, and ensures that you always use the latest version of a generator or build tool without having to upgrade each time you're about to use it.
Hence, for what we are about to learn, instead of doing something like
$ sudo npm install create-react-app -g
you will want to do
$ npx create-react-app my-app
every time you want to make a new react
application using create-react-app
. Using npx
in this fashion ensures you are getting the latest version of create-react-app
instead of installing create-react-app
globally and subsequently possibly using an outdated version.
Modernizing our web development with create-react-app
Up until now, we have used actual .html
files and actual .js
files and we connected the JavaScript to our HTML by means of script
tags. Nothing is wrong with this, and it's probably rather important to start learning React this way; otherwise, you risk it being very mystical how React is actually working under the hood. But this is not how modern React development is done. Instead, Facebook has made (and maintains) create-react-app which is a node module available through NPM. And this thing does a crazy good job of kicking out everything you could possibly need to build out a great React application from scratch. Building production applications before this came out was highly frustrating (due to the amount of configuration that was needed in so many respects).
We are now going to fire off a React application from a scaffold using create-react-app
. And if you've used, say, Rails before, Rails creates a scaffold for you when you do rails g, Laravel is similar with php artisan
, the Express generator is similar with express-generator
, and so on. Most frameworks and platforms have something like this where it will just create a whole bunch of files for you (i.e., a "scaffolding"). It's kind of like saying, "Developers need this to make almost anything so we'll just give you what you probably need and you can take it from there."
We will run
$ npx create-react-app first-cra
where first-cra
simply stands for "First Create-React-App." This process will probably take a decent bit of time because a ton of dependencies are being installed. We can then cd first-cra
and run npm start
as we are informed in the terminal and we'll get a boilerplate React application launched on localhost:3000
(or we will be prompted for another port on which to listen if that port is already in use). Now, if you look at the node_modules
folder, you will see hundreds of node modules. The webpack
node module is what creates the development server for us. We also see a whole bunch of jest
node modules. And jest
is a unit-testing framework for JavaScript, and React has been kind enough to install everything to get that up and running if we choose to do testing (as we should!). We also see a whole bunch of eslint
node modules. And those are meant to make error messages when we run our code to be much friendlier and much more helpful.
Now let's inspect our package.json
for a moment:
{
"name": "first-cra",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Recall that the package.json
file gives metadata about our application; that is, it tells the world about our application. And one of the most important things the package.json
file does is it determines your dependencies, meaning if someone downloads your app and wants to use it, you have to have the node modules listed in the dependencies:
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
}
As for why npm start
worked to launch our development server, we can see this from the scripts
part in our package.json
:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
And we can actually check these scripts out for ourselves if we want:
node_modules -> react-scripts -> scripts -> build.js | eject.js | init.js | start.js | test.js
So the scripts
folder in the react-scripts
node module is where all these little scripts live. When we type npm start
, npm
is going to go check out our package.json
file and see if there is a start
command. There is! So it's going to call the react-scripts
node module and it's going to grab the start
script that lives in start.js
. Basically, this is where Babel, Webpack, and basically all of the development stuff is going to get started. And you can check out build.js
for the build
script as well. And in react-scripts
we see a package.json
file which details that node module's dependencies, several of which have their own dependencies, and so forth (that is how we end up with a huge folder of node modules at the root of our application).
Apart from the testing modules, React needs react
, react-dom
, and react-scripts
, the last of which actually creates our entire development environment for us.
Understanding the file structure given to us by create-react-app
As of this writing (April 26, 2020), this is the file structure you will get when running create-react-app
on cra-app-name
:
cra-app-name
┣ node_modules
┣ public
┃ ┣ favicon.ico
┃ ┣ index.html
┃ ┣ logo192.png
┃ ┣ logo512.png
┃ ┣ manifest.json
┃ ┗ robots.txt
┣ src
┃ ┣ App.css
┃ ┣ App.js
┃ ┣ App.test.js
┃ ┣ index.css
┃ ┣ index.js
┃ ┣ logo.svg
┃ ┣ serviceWorker.js
┃ ┗ setupTests.js
┣ .gitignore
┣ README.md
┣ package-lock.json
┗ package.json
We are going to spend almost all of our time in the src
folder. The entry point for the entire application is index.js
inside of src
:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
This is where everything is going to start. And some of it may look a little funky, but at least one bit should look very familiar (read more about React.StrictMode
on Medium or directly from the docs; the gist is that StrictMode
is a tool for highlighting potential problems in the application and, like Fragment
, StrictMode
does not render any visible UI--it simply activates additional checks and warnings for descendants):
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
Above, we are rendering a component call App
(in StrictMode
) and we are putting in whatever HTML element has an id
of root
. But where is React actually getting root
at? It is getting it from index.html
which appears in the public
folder. Note that anything placed in the public
folder will be publicly accessible. All of its contents will be statically served--it won't be part of your React application (unless it's a link or something like that). This is where you would typically place files like images, audio clips, videos, JavaScript that is not part of React (like a library or your own utility files), CSS files, etc. The point, however, is that index.html
is located in this public
folder and we see <div id="root"></div>
contained therein so that is where everything is going from ReactDOM.render
in our index.js
file in the src
folder.
At the top of our index.js
file we see the following:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
These are all import
statements and this is an ES6+ feature which is not supported by any browser (at least as of this writing). import
makes it very easy to modularize your stuff. The point is that import
is kind of like doing
import React from 'react';
instead of
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
as we have been doing previously. Using import React from 'react';
indicates we are fetching the React
object from the react
node module. So the react
node module had better exist! The same thing applies to ReactDOM
coming from react-dom
. We brought these in before by adding React
and ReactDOM
to the window
object by our script
tags, but now they're being added by Node.js through Webpack. We can also import our index.css
easily with one line. And we can import App
from ./App
exactly as we just described instead of doing something like
<script src="./App.js"></script>
as before. Well, what actually lives in App.js
? Let's see:
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
import
a new component into App.js
before you make it so you can benefit from the linter
When making a new component, it is often wise to go ahead and import that component in your App.js
so that any issues you encounter while making your component are caught by the linter and you can be warned about them.
By default, if you are not importing your component in App.js
, then the linter will not be able to catch any problems in your component since your component is not actually being loaded into or looked at in App.js
.
What is state?
Previously, we talked about components and about props. Components form the bedrock of React and props is how we pass data to components. The third main staple or backbone element of React is the concept of "state." As discussed previously, React came onto the web development scene with a couple of goals in mind, the first being to modularize web development because modern web development has massive front-ends that are getting totally out of control. The codebases are huge and have to be modularized. Components solve that. We need to speed up the front-end in the browser, and the virtual DOM solves that by minimizing the things that actually need to change. The other really big thing that Facebook wanted to do when they made React was create some kind of state management tool.
State is a fairly universal concept in programming. An oversimplified definition might be that state is the value of a variable or variables at any given time. And the whole issue of state management is really a problem in modern web development. An example to illustrate this might be by imagining your daily vist to Amazon's website. We can describe the process in different states:
- State 1: What happens when you order a product on their site? What are the different steps of the process? Maybe you start by having an empty cart. You've never been there before and maybe we say that you have a pristine empty cart.
- State 2: You place an item in your cart. Suppose you add a super cool book about gardening to your cart, but then soon after decide you don't want the book so you remove the book from your cart. Does that take you back to State 1 where you had a pristine empty cart? No. It does not. It does not take you back to State 1. Why not? Because, in Amazon's eyes, you now have a dirty empty cart.
- State 3: By dirty empty cart what is meant is that something used to be in the cart and is no longer there. The difference between a pristine empty cart and a dirty empty cart is very important to Amazon. Why? Because they will want to market to you as you cruise around the site (e.g., "Hey, remember that gardening book you were thinking about buying? You should buy that! Or maybe these related books."). They are going to want to keep track of what you put in and take out of your cart.
- State 4: Maybe this time you add a book to your cart on woodworking because gardening just wasn't doing it to you. Are we now in the same situation we were in during State 2? No, of course not. The books are different.
- Other States: You can imagine other states being things such as maybe you go to the checkout, the payment section, the delivery phase, the customer service phase, etc. And maybe at some point you need to return something. The idea is that there's any number of things that can happen during this whole process, and Amazon whats to track this carefully.
If you think about a simple web form, the same thing will be true. Maybe you have a super simple form with a username
entry, a password
enty, and a submit
button. As soon as you type something into the username
or password
fields, something has just happened. The DOM has just changed, however slightly. The value
of an input
box in the DOM has just changed. Every time we have changed something in the DOM we have mutated the DOM. For a tiny form, the changes might be totally nominal. Maybe we have two password fields, the second to verify the first and vice-versa. Another thing we might want to know is how many times this particular form has been changed: does it seem like human behavior or robot behavior? Have they submitted the form several times? And so on. So there's lots of options and there's lots of reasons to be interested in how the page is actually changing.
Another example would be something like playing Tic-Tac-Toe in JavaScript (in fact, one of the main tutorials on first learning React uses this as an example of highlighting all of the key concepts in React). This would also have state. What is the state of the game right now? What place is occupied by which player? Whose turn is it? And so on.
The bottom line is that there needs to be some state management tool so that we can figure out where certain things are in a process because the application needs to make decisions about what the current state is, and we want to avoid making mutations (i.e., updating the DOM) when they're not necessary.
Instead of manipulating the DOM directly, we will allow components to manage their themselves, and we can pass that around as needed to other components. We will use functional programming to update the state which means we won't ever change state directly--we will let React know that something happens that way React can go through all of its paces and all the right channels to make sure that whoever needs to know or whatever needs to happen can happen in the appropriate order.
A note about React.StrictMode
double-invoking functions (constructor
, render
, etc.)
Since
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
is part of the new boilerplate code added to index.js
in the src
folder when scaffolding out a project using create-react-app
, it's good to know that some mildy funky or unexpected behavior can result from using React.StrictMode
. Specifically, if we make a dummy component like
import React, { Component } from 'react';
class StateInAction extends Component {
constructor(props) {
super(props);
console.log('Constructor is running')
}
render() {
return (
<h1>State in Action</h1>
);
}
}
export default StateInAction;
and drop this in App.js
like so:
import React from 'react';
import './App.css';
import StateInAction from './components/StateInAction/StateInAction.component';
class App extends React.Component {
render() {
return (
<div className="App">
<StateInAction />
</div>
);
}
}
export default App;
Then we will actually see Constructor is running
logged to the console twice. And if we change our index.js
to only have
ReactDOM.render(
<App />,
document.getElementById('root')
);
as used to be the case when creating projects with create-react-app
, then Constructor is running
will only be logged once. What accounts for this behavior?
This answer on Stack Overflow succinctly answers this question and points us to the docs for more information. Essentially (from the SO post), in recent versions of React, rendering uses strict mode when running in development. Strict mode intentionally double-calls the constructor
and render
functions to better detect unexpected side effects. From the docs: Strict mode can't automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
- Class component
constructor
,render
, andshouldComponentUpdate
methods - Class component static
getDerivedStateFromProps
method - Function component bodies
- State updater functions (the first argument to
setState
) - Functions passed to
useState
,useMemo
, oruseReducer
Running in production build at least in one use case did not result in the same double render of the class component.
Getting started with state (where to initialize, do's and don'ts, etc.) and events (binding in constructor
, etc.)
State (initializing and setting/updating)
You will want to initialize state in the constructor
method of your class component underneath super
:
constructor(props) {
super(props);
this.state = {
// ...
}
}
The this
in our case is the class instance itself because we are inside of the constructor
. And state
is an instance variable or it's like a variable for this particular object. State is very special. You can make anything you want inside of your constructor just like you can in any other language. But the state
variable is very very special and unique to React. We can start by defining a property for our state
object:
this.state = {
text: 'State in Action!'
}
And we can use it like so:
import React, { Component } from 'react';
class StateInAction extends Component {
constructor(props) {
super(props);
this.state = {
text: 'State in Action!',
};
}
render() {
const { text } = this.state;
return <h1>{text}</h1>;
}
}
export default StateInAction;
Just to illustrate how to update state for the moment, even though we will probably never do this in the future (a whole new world will open up once we start exploring events), we can set up a setTimeout
in our constructor
:
constructor(props) {
super(props);
this.state = {
text: 'State in Action!',
};
setTimeout(() => {
this.setState({
text: 'State Changed!'
})
}, 2000)
}
This method will run one time (when the component is created). We'll run super
which will get us everything great about being a React component, this.state
will set up our local state
variable, and setTimeout
will get called. Note how we don't try to manually change state
by doing something like this.state.text = 'State Changed'!
. We never mutate state
manually by ourselves. As the docs note in the context of certain things we should know about setState
:
- Do not modify state directly. [Read more.]
- State updates may be asynchronous. That is, React may batch mulitple
setState
calls into a single update for performance--becausethis.props
andthis.state
may be updated asynchronously, you should not rely on their values for calculating the next state. To account for this, you should use a second form ofsetState
that accepts a function rather than an object, and that function will receive the previous state as the first argument and the props at the time the update is applied as the second argument. See the docs for more, this Medium post, this Stack Overflow thread, and the bottom of this article by Tyler McGinnis where he notes that, "It's rarely used (i.e., the second form ofsetState
where you pass a function) and not well known, but you can also pass a function tosetState
that receives the previous state and props and returns a new state, just as we're doing above. And not only is nothing wrong with it, but it's also actively recommended if you're setting state based on the previous state." So if you are setting state based on previous state (e.g., counters and the like or a whole host of other things), then passing a function tosetState
instead of just an object is what you will want to do. [Read more.] - State updates are merged. [Read more.]
As alluded to above, even though something like this.state.text = 'State changed'
looks fine with regular old JavaScript, this is something we never do in React (i.e., never set state
manually via an assignment like the above). The only time you should be doing this.state = ...
is inside of the class constructor
when the component is created for the very first time. The reason for that (some of which is mentioned above in the three different points) is because React needs to do a whole bunch of stuff under the hood when the state changes. So instead of changing the state ourselves, we can hand it to React and React can run its own method (i.e., setState
), do a whole bunch of stuff, and ultimately change it for us. Why all this rigmarole? In object-oriented programming, something like setState
is called a setter
(it is the counter-companion to a getter
; read more about setters and getters on MDN). A setter is a function whose job is to mutate something else (i.e., set something and implement how that something should be set without exposing the innerworkings of how everything is set to the user or developer; in this context, we want to set the state
in some fashion, and React wants to handle how the state is set by exposing its setState
method on the React.Component
prototype (remember our class inherits all the goodies from the Component
class since our class extends React.Component
)). So setState
is basically saying, "Hey, if you want to change a state
variable, then you tell me, and I will change it for you. Don't change it yourself."
Events (getting started)
Events in React in General
As the docs detail in regards to handling events (something we will address in more detail in the next note), handling events with React elements is "very similar" to handling events on DOM elements. There are two key syntactical differences:
- React events are named using camelCase, rather than lowercase.
- With JSX you pass a function as the event handler, rather than a string.
Hence, instead of something like
<button onclick="activateLasers()">
Activate Lasers
</button>
as would normally be the case, in React, we would have something like
<button onClick={activateLasers}>
Activate Lasers
</button>
The docs also note that you cannot return false
to prevent default behavior in React. Instead, you have to call preventDefault
explicitly. They give an example in plain HTML of preventing the default link behavior of opening a new page:
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>
In React, we would have something like the following instead:
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
What we care about here, however, is not just preventing default behavior (which is no doubt nice to know) but how events are treated in general in React (the docs note that the e
referenced above is a SyntheticEvent
, something we will address in just a moment). Remember that React elements are not DOM elements until they are made so by a need to update the DOM. In particular, the JSX we use is transpiled by Babel into JavaScript the browser can understand in order to turn our newly created React elements into freshly minted DOM elements. So how do event listeners work in React? Typically, without React, you would use the addEventListener Web API for Event Targets to add a listener to a DOM element after it was created. Consider this silly example (adding a button to the top of body
in the DOM which, when clicked, alert
s hello!
):
let newElement = document.createElement('button');
let buttonText = newElement.innerText;
newElement.innerText = 'Click me to hear me say hello!'
newElement.addEventListener('click', (e) => {
console.log(e); // MouseEvent { ..., type: "click", ... }
alert('hello!');
})
document.body.prepend(newElement);
Here we have a "normal" event e
that is a MouseEvent
of type click
. In the React example, they noted that e
is a synthetic event? Well what the heck is a synthetic event? Their docs give more information, but here are some observations:
The
SyntheticEvent
wrapper forms part of React's Event System.Event handlers will be passed instances of
SyntheticEvent
, a cross-browser wrapper around the browser's native event.React normalizes events so that they have consistent properties across different browsers. The event handlers below are triggered by an event in the bubbling phase. To register an event handler for the capture phase, append
Capture
to the event name; for example, instead of usingonClick
, you would useonClickCapture
to handle the click event in the capture phase.
If we click on the Mouse Events link, then we will see the React onClick
event which effectively maps to placing addEventListener
on an element and listening for an event of type click. Functionality wise, the code
let newElement = document.createElement('button');
let buttonText = newElement.innerText;
newElement.innerText = 'Click me to hear me say hello!'
newElement.addEventListener('click', (e) => {
console.log(e); // MouseEvent { ..., type: "click", ... }
alert('hello!');
})
document.body.prepend(newElement);
in plain JavaScript and
<button onClick={(e) => {console.log(e); alert('hello!')}}>Click me to hear me say hello!</button>
in JSX when using React are the same. In fact, when we log e
to the console in the React example when the button is clicked we see Class { ..., nativeEvent: MouseEvent, type: "click", ... }
.
SO WHAT IS THE POINT OF ALL THIS TEDIUM? The basic point is this: React may not handle events exactly how you are used to or how you expect. In particular, when you define a component using an ES6 class, a common pattern in React is for an event handler to be a method on the class (since for JSX we pass functions as event handlers rather than strings). Where this pattern can get a little tricky and confusing, however, is understanding the meaning of this
in reference to the functions we pass as event handlers to React elements (i.e., the functions we use as JSX callbacks).
Possibilities for Confusion: When Event Handlers are Class Methods (the usual)
Consider the following example from the docs that illustrates where confusion may arise when using a class method as an event handler (as the docs also note, this is a very common pattern in React development so we need to make sure we understand what is going on):
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// This binding is necessary to make `this` work in the callback
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
What's happening here? Why do we need the this.handleClick = this.handleClick.bind(this);
line in the constructor
(try commenting that line out and clicking the button; the application will break and you'll get a nasty error implicitly communicating that this
is undefined
: TypeError: Cannot read property 'setState' of undefined
)? As the docs note, "Class methods in JavaScript are not bound by default. So if you forget to bind this.handleClick
and pass it to onClick
, this
will be undefined
when the function is actually called." But why? What would this
normally (i.e., outside of React) refer to in regards to DOM event handlers? As the MDN docs note about this
(towards the bottom there are two subsections: this
"As a DOM event handler" and this
"In an inline event handler"): "When a function is used as an event handler, its this
is set to the element on which the listener is placed (some browsers do not follow this convention for listeners added dynamically with methods other than addEventListener()
). [...] When the code is called from an inline on-event handler, its this
is set to the DOM element on which the listener is placed."
Here is the key question of concern if we are to have any hope of understanding why it is necessary (depending on our approach) to bind event handler class methods in the constructor
in React: How is this
set in regards to event handlers for React elements? To answer this question, we have to dive under the hood a bit, but the result is worth it. What follows is largely a paraphrased version of this excellent answer on Stack Overflow.
React Event Handlers: Understanding the Meaning of this
We can better understand how React treats event handlers by considering and pondering a few key points in succession:
this
is dynamic- how React handles event handlers
- why
bind
ing works - why arrow function properties work
- recap: 4 examples
- main takeaway
this
Is Dynamic
To better understand the React-specific situation, a brief introduction to how this
works is in order; in particular, we will want to observe how this
can lose meaning (i.e., become undefined
) when the method referring to this
gets passed around.
One of the keys to understanding this
in JavaScript lies in knowing that this
is a runtime binding and depends on the current execution context, hence why this
is commonly refered to as "context". Sometimes (not just in React) we have to specify the context in which we want this
to be considered (i.e., give information on what we want to be the current execution context when this
is referenced) in order to get desired/expected results. Binding is necessary on occasion because you sometimes you lose "context" (this is especially true when dealing with React event handlers, as we will soon see).
To clearly illustrate how this problem (i.e., losing "context") can arise, consider the following basic snippet:
const myDog = {
name: 'Archie',
sayName: function() {
return this.name;
}
}
console.log(myDog.sayName()); // 'Archie' ... all good so far!
In this example, we get 'Archie'
, as expected. But consider this example:
const sayMyDogName = myDog.sayName;
console.log(sayMyDogName()); // undefined ... Uh oh! Why!?!?
It may be unexpected to find that we get undefined
logged and thrown back in our faces--where did 'Archie'
go? The answer lies in "context" or how you execute a function. Compare how we call the functions:
// Example 1
myDog.sayName();
// Example 2
const sayMyDogName = myDog.sayName;
sayMyDogName();
Notice the difference. In the first example, we are specifying exactly where the sayName
method is located (we'll use "method" to refer to a function that is supposed to be bound to an object, and "function" for those not): on the myDog
object:
myDog.sayName();
^^^^^
But in the second example, we store the method into a new variable, and then we use that variable to call the method without explicitly state where the method actually exists, thus losing context:
sayMyDogName(); // which object is this function coming from?
And therein lies the problem. When you store a method in a variable, the original information about where that method is located (i.e., the "context" in which the method is being executed) is lost. Without this information, at runtime, there is no way for the JavaScript interpreter to bind the correct this
. Without specific context, this
does not work as expected (in the second snippet, undefined
is logged instead of 'Archie'
because this
defaults to the global execution context (window
when not in strict mode, or else undefined
) when it cannot be determined via specific context; and in our example window.name
does exist thus yielding undefined
).
To fix this issue in our own example, we can use Function.prototype.bind() method: "The bind()
method creates a new function that, when called, has its this
keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called." So instead of
const sayMyDogName = myDog.sayName;
console.log(sayMyDogName()); // undefined ... Uh oh! Why!?!?
we can create a new function (i.e., sayMyDogName
) that, when called, has its this
keyword set to myDog
:
const sayMyDogName = myDog.sayName.bind(myDog);
console.log(sayMyDogName()); // 'Archie' ... yay! Who's a good boy!?
How React Handles Event Handlers
Here is an example of a React component suffering from the dreaded error resulting from the this
problem: TypeError: Cannot read property 'setState' of undefined
:
import React from 'react';
class CountExperiment extends React.Component {
constructor(props) {
super(props);
this.state = {
clicks: 0,
};
// this.handleClick = this.handleClick.bind(this); // <-- needed to work properly; why?
}
handleClick() {
this.setState((prevState, props) => ({
clicks: prevState.clicks + 1,
}));
}
render() {
return <button onClick={this.handleClick}>{this.state.clicks}</button>;
}
}
export default CountExperiment;
But why and how does the previous subsection about "this
is Dynamic" relate to the this
issue we have here? Because the component above and "Example 2" considered previously both suffer from an abstraction of the same problem. Specifically, note how React handles event handlers:
// Edited to fit this description; React performs other checks internally
// props is the current React component's props,
// registrationName is the name of the event handle prop (e.g., "onClick")
let listener = props[registrationName];
// Later, listener is called
So when you put onClick={this.handleClick}
in your JSX in React, the this.handleClick
method is eventually assigned to the variable listener
(if you go down the rabbit hole of how events in the event queue are executed, invokeGuardedCallback is called on the listener
). But now you can hopefully see why we have the problem we do: since React has assigned this.handleClick
to listener
, we are no longer specifying exactly where handleClick
is coming from! From React's point of view, listener
is just some function, not attached to any object (the CountExperiment
React component instance in this case). This is similar to what happened in our much simpler previous example:
const myDog = {
name: 'Archie',
sayName: function() {
return this.name;
}
}
const sayMyDogName = myDog.sayName;
console.log(sayMyDogName()); // undefined
From JavaScript's point of view, sayMyDogName
is just some function, not attached to any object (the myDog
object in this case).
The takeway: In both situations (sayMyDogName
with myDog
and handleClick
with CountExperiment
), we have lost context and thus the interpreter cannot infer a this
value to use inside the sayName
method in the myDog
object and the handleClick
method in the CountExperiment
object (i.e, React component instance).
Why bind
ing Works
From everything we have discussed, it is sensible to wonder: If the interpreter decides the this
value at runtime, then why can I bind the handler so that it does work? The reason is because you can use Function#bind
to guarantee the this
value at runtime (specificially, we want to guarantee that the the this
for our event handler points to the class instance or new React component). This is done by setting an internal this
binding property on a function, allowing it to not infer this
(we want to set this
*explicitly):
this.handleClick = this.handleClick.bind(this);
When the line above is executed, presumably in the class constructor
, the current this
is captured (i.e., the React component instance) and set as an internal this
binding of an entirely new function, returned from Function#bind
. The bind
method ensures the interpreter will not try to infer anything about the value of this
when this
is being calculated at runtime but explicitly use the provided this
you have given it.
To see this in action in a semi-illustrated fashion, consider the following component:
import React from 'react';
class IllustratedBinding extends React.Component {
constructor(props) {
super(props);
this.state = {
unboundedClicks: 0,
boundedClicks: 0
}
this.handleClickBound = this.handleClickBound.bind(this);
// |____|
// ^----- React component instance
}
handleClickUnbound() {
this.setState((prevState, props) => ({
unboundedClicks: prevState.unboundedClicks + 1
}));
}
handleClickBound() {
this.setState((prevState, props) => ({
boundedClicks: prevState.boundedClicks + 1
}));
}
render() {
return (
<React.Fragment>
{/* v----------------- not an event handler (so never assigned as "listener")
|_________________| */}
<p onLoad={console.log(this)}></p>
<button onClick={this.handleClickUnbound}>handleClickUnbound</button>
{/* |_______________________|
^-------------- this.handleClickUnbound eventually assigned as
"listener", thus losing context */}
<button onClick={this.handleClickBound}>handleClickUnbound</button>
{/* |_____________________|
^--------------- this.handleClick eventually assigned as "listener",
but context not lost since context was explicitly
bound to the current "this" captured in the constructor
method (i.e., the React component instance) */}
</React.Fragment>
)
}
}
export default IllustratedBinding;
Although all the details (and comments) are included in the code above, it's worth noting the following:
<p onLoad={console.log(this)}></p>
is simply included in order to show that referring tothis
within a class results in what is expected, namelyIllustratedBinding { ... }
. It's when you try to usethis
in event handlers that we have to be mindful of how our event handler class method will eventually get assigned as "listener" in the future.handleClickUnbound
: As its name implies, thehandleClickUnbound
method has not been bounded. Hence, when the button is clicked on which this method is passed as the callback, we will get the dreadedthis
error:TypeError: Cannot read property 'setState' of undefined
. The reason is becausethis.handleClick
is eventually assigned tolistener
, thus losing its context (i.e., the React component instance).handleClickBound
: As its name implies, thehandleClickBound
method has been bounded. Specifically, what is the currentthis
in the classconstructor
(or anywhere else in the class for that matter)? Thethis
in the class constructor refers to the React component instance we are creating. Hence, we capture the currentthis
in the constructor viabind
: By declaringthis.handleClickBound = this.handleClickBound.bind(this);
in theconstructor
, we ensure wheneverthis.handleClickBound
is called that thethis
being referred to withinhandleClick
points to the React component instance we are creating.
A slightly more cleaned up "visual" example of the above is given below as an image:
Why Arrow Function Properties Work
Arrow function class properties currently work through Babel based on, as an example,
handleClick = () => { /* Can use this just fine here */ }
being effectively transpiled into
constructor() {
super();
this.handleClick = () => {}
}
And this works due to the fact that arrow functions do not bind their own this
but take the this
of their enclosing scope, namely the constructor
's this
in this case which points to the React component instance, thus giving us the correct this
.
It's actually a lot more complicated. React internally tries to use Function#bind
on listeners for its own use, but this does not work with arrow functions as they simply do not bind this
. That means when this
inside the arrow function is actually evaluated, the this
is resolved up each lexical environment of each execution context of the current code of the module. The execution context which finally resolves to have a this
binding is the constructor
, which has a this
pointing to the current React component instance, allowing it to work.
Recap: 4 Examples
Now that we have a much better understanding of how event handlers work in React and why it's necessary to bind
methods in the constructor
(unless you bind
elsewhere which is atypical and not recommended or you use arrow functions which frequently occurs and is recommended), we can consider 4 approaches or examples of how to go about defining event handlers in React. The following component is meant to illustrate these examples:
import React from 'react';
class FourExamples extends React.Component {
constructor(props) {
super(props);
this.state = {
isToggleOnWithoutBind: true,
isToggleOnWithBind: true,
isToggleOnWithArrow: true,
isToggleOnWithArrowCallback: true,
};
// For explicit binding in Example 2, as we learned was needed
this.handleClickWithBind = this.handleClickWithBind.bind(this);
}
handleClickWithoutBind() {
this.setState((prevState, props) => ({
isToggleOnWithoutBind: !prevState.isToggleOnWithoutBind
}));
}
handleClickWithBind() {
this.setState((prevState, props) => ({
isToggleOnWithBind: !prevState.isToggleOnWithBind
}))
}
handleClickWithArrow = () => {
this.setState((prevState, props) => ({
isToggleOnWithArrow: !prevState.isToggleOnWithArrow
}));
};
handleClickWithArrowCallback() {
this.setState((prevState, props) => ({
isToggleOnWithArrowCallback: !prevState.isToggleOnWithArrowCallback
}));
}
render() {
const buttonMargin = {
display: 'block',
marginTop: '5px',
marginBottom: '20px',
};
const divStyle = {
textAlign: 'left',
marginLeft: '10px',
fontFamily: 'monospace',
fontSize: '20px',
};
return (
<div style={divStyle}>
{/* Example 1 (incorrect!!!) */}
handleClickWithoutBind:
<button style={buttonMargin} onClick={this.handleClickWithoutBind}>
{this.state.isToggleOnWithoutBind ? 'ON' : 'OFF'}
</button>
{/* Example 2 (use explicit binding: correct) */}
handleClickWithBind:
<button style={buttonMargin} onClick={this.handleClickWithBind}>
{this.state.isToggleOnWithBind ? 'ON' : 'OFF'}
</button>
{/* Example 3 (exploit fact that arrow functions do not bind their own "this": correct) */}
handleClickWithArrow:
<button style={buttonMargin} onClick={this.handleClickWithArrow}>
{this.state.isToggleOnWithArrow ? 'ON' : 'OFF'}
</button>
{/* Example 4 (works but not performant since different callback is created for each render) */}
handleClickWithArrowCallback:
<button style={buttonMargin} onClick={() => this.handleClickWithArrowCallback()}>
{this.state.isToggleOnWithArrowCallback ? 'ON' : 'OFF'}
</button>
</div>
);
}
}
export default FourExamples;
And you can see the results below:
Other class methods defined in this manner should be similarly bound in the constructor
. See this article for more about how bind
works as well as this Tyler McGinnis article for understanding this
, especially in the context of the call
, apply
, and bind
functions.
Main Takeaway
In most cases, you should either bind
your class methods in the constructor
:
class SampleClass {
constructor(props) {
super(props);
this.state = { ... };
this.oneMethod = this.oneMethod.bind(this);
this.twoMethod = this.twoMethod.bind(this);
this.threeMethod = this.threeMethod.bind(this);
...
}
oneMethod() { ... };
twoMethod() { ... };
threeMethod() { ... };
...
render() {
return (
// JSX
)
}
}
Or you should simply use public class fields syntax with arrow functions:
class SampleClass {
oneMethod = () => { ... };
twoMethod = () => { ... };
threeMethod = () => { ... };
...
render() {
return (
// JSX
)
}
}
Events in React
Now that we have a sense of what state is and the basics of how it works, the question becomes: Why would we ever want to use it? When is state really useful? We're going to answer that now. The short answer lies in the fact that React is JavaScript, and it's a UI that runs in the browser, and JavaScript is an event-based language. What causes something to happen on any webpage? What causes the UI or DOM to change? It's usually because the user clicked on something, submitted a form, changed an input box, etc. So we are going to talk about how events work now because state and events are very closely related. Of course, state can change at any point inside of an application or a component, but it's most commonly going to change with events.
If we create a simple button, then historically we might do something like the following to add an event listener to it:
import React, {Component, Fragment} from 'react';
class SimpleEvents extends Component {
constructor(props) {
super(props);
this.state = {
msg: 'stateActivated'
}
}
render() {
document.querySelector('.btn').addEventListener('click', () =>{
console.log('Button was clicked!')
})
return (
<Fragment>
<button onclick="myFunction()">Click me!</button>
</Fragment>
);
}
}
export default SimpleEvents;
This is a very native JavaScript way of doing this. But what's the problem with what we've done above? We'll get an error: TypeError: Cannot read property 'addEventListener' of null
. We are getting this error because, in React, our button
actually hasn't been added to the DOM yet. So we can't do addEventListener
because, well, the first time render
runs there's nothing to add an event listener on (the JSX hasn't been returned yet). So we can't use addEventListener
right now in this way (we can later if we want due to to how the component lifecycle works in React).
The other way you would normally do something like the above is you would add the event listener directly to the element we wanted to be listening on:
<button onclick="myFunction()">Click me!</button>
This is very similar to how we do things in React (with a couple of caveats). In React, virtually every DOM event is still available, but we use camelCase, and we hand the event the callback we want to run:
<button onClick={myFunction}">Click me!</button>
Unlike in native JavaScript, we don't invoke functions as part of the event listener but instead pass functions, as seen above. We pass a callback in our JSX that is going to get run later. The reason for this is pretty simple: In native JavaScript we invoke the code we want to run later (as in onclick="myFunction()"
, for example), but in React we don't invoke code we pass code. For something like
<button onClick={console.log('Test')}>Click me!</button>
what we pass to onClick
is going to get evaluated, and we don't return anything in the example immediately below (technically, a function returns undefined
in JavaScript when nothing else is explicitly returned). So what's actually sent back is undefined
as a result of console.log('Test')
. Hence, on the first render
, we get Test
logged to the console, but every time thereafter we essentially have onClick=undefined
. So nothing will really happen when we click on the button because we're technically not telling React to do anything when we click it. The overall point is that we do not run code in onClick
or other listeners for events; instead, we pass code. We pass a callback. And we can do this in two ways.
Instead of running our code as above, we can simply create a new anonymous function that runs the code for us:
<button onClick={() => console.log('Test')}>Click me!</button>
Using the code above will result in us logging Test
to the console every time the button is clicked, as desired. This kind of pattern is fairly common in React. Again, we are not running console.log('Test')
--we are passing a function that runs console.log('Test')
. In general, this kind of pattern, albeit common, is also a bit messy. If possible, it's often wise to define a class method, which we can then pass by name:
import React, {Component, Fragment} from 'react';
class SimpleEvents extends Component {
constructor(props) {
super(props);
this.state = {
msg: 'stateActivated'
}
}
handleClick() {
console.log('Test');
return console.log('Real Test')
}
render() {
return (
<Fragment>
<button onClick={this.handleClick()}>Click me!</button>
</Fragment>
);
}
}
export default SimpleEvents;
This is functionally the same as what we had before. Again, we are not going to run this.handleClick
but pass this.handleClick
. What we can't do is this.handleClick()
because this will just result in Test
being logged to the console once when the inital render
occurs, but this.handleClick()
returns undefined
. What if we tried to do something kind of silly like
// ...
handleClick() {
console.log('Test');
return console.log('Real Test')
}
//...
<button onClick={this.handleClick()}>Click me!</button>
// ...
Would this work? What would happen? Well, instead of returning undefined
now, we would return console.log('Real Test')
which itself returns undefined
; hence, we would just end up with Test
and Real Test
being logged to the console in succession. The "fix" (only for the sake of illustration ... you would never want such a silly function!):
// ...
handleClick() {
console.log('Test');
return () => console.log('Real Test')
}
//...
<button onClick={this.handleClick()}>Click me!</button>
// ...
Then we would get Test
logged on the first render and Real Test
logged every time we clicked the button thereafter.
In summary, those are the two ways:
- We can define a method and pass that method.
- We can define an anonymous function within the React element itself that runs our code.
Now let's consider a different kind of element, an input
. You don't really click on input
other than to change it. So with an input
we will use onChange
:
<input type="text" placeholder="Enter some text!" onChange={this.handleChange} />
And if we want to, we can have a simple
handleChange() {
console.log('User changed the input')
}
And as a user types this handler will fire every single time the user presses a key because any time something changes inside of the input
box we are logging User changed the input
to the console. We can have more fun by using something like
handleChange(e) {
console.log(e.target.value)
}
which will actually what's currently in the input
box.
Let's now consider a form
to round things out. You don't click on or change forms--you submit
them. So we will use onSubmit
. If we wrap our previous elements in the form
then something interesting will happen:
handleSubmit() {
console.log('Form submitted!')
}
render() {
return (
<Fragment>
<form onSubmit={this.handleSubmit}>
<button onClick={this.handleClick}>Click me!</button>
<input
type="text"
placeholder="Enter some text!"
onChange={this.handleChange}
/>
</form>
</Fragment>
);
}
If we hit Enter
while inside of the input
box, then you will very briefly see Form submitted!
logged to the console. Why does this message disappear? What's happening is common to JavaScript (in other languages there are mechanisms to prevent this), namely we have to manually tell the browser to not send the form forward to the next page because we don't have one. Normally your form
tag might have an action
or a method
on it (e.g., action='/process_review_submission'
with method='post'
). But for our case right now, we want to stop the form from being sent forward. A very important topic when it comes to handling events is that every event in React some with an event
object that will automatically be passed (even if we don't use it as we haven't so far except in the example of using e.target.value
, where e
is referring to the event). The event that is passed in each case comes with a preventDefault
method which ... prevents the default behavior of what the event normally does, which, in the case of a form submission, is to send the form forward to the next page. We are going to prevent that default behavior by using e.preventDefault
within our handleSubmit
handler.
Available events
If we head over to SyntheticEvent in the docs, we can see what all events are available in React, and they use the SyntheticEvent
wrapper (essentially a class) in order to try and normalize the way every browser works with events, just meaning that, for example, Firefox may have a slightly different event object than Chrome does, than Safari does, and so on. Instead, React uses the SyntheticEvent
which makes sure that they pretty much all behave the same way regardless of the browser.
If you go down to Supported Events, you will see that they have categorized them:
- Clipboard Events
- Composition Events
- Keyboard Events
- Focus Events
- Form Events
- Generic Events
- Mouse Events
- Pointer Events
- Selection Events
- Touch Events
- UI Events
- Wheel Events
- Media Events
- Image Events
- Animation Events
- Transition Events
- Other Events
And we can take a glance at what event names there are for a given category; for example, we get the following for Mouse Events:
- onClick
- onContextMenu
- onDoubleClick
- onDrag
- onDragEnd
- onDragEnter
- onDragExit
- onDragLeave
- onDragOver
- onDragStart
- onDrop
- onMouseDown
- onMouseEnter
- onMouseLeave
- onMouseMove
- onMouseOut
- onMouseOver
- onMouseUp
There are the Pointer Events for when you actually move your cursor around, and so on. So the Synthetic Events page can be a great reference to refer to if you are wondering how a particular event works.
Changing state with an event
We are now going to tie the two together (i.e., state and events and how we can let events change state and thereby the UI). The best way to see how state and events can be tied together is by considering a basic example involving them (see if you can tell what is intended inside of the component):
import React, { Component, Fragment } from 'react';
class EventAndState extends Component {
constructor(props) {
super(props);
this.state = {
titleText: '',
inputText: '',
};
}
handleClick = (e) => {
this.setState({
titleText: 'Try some more possibilities!',
inputText: e.target.value,
});
};
handleChange = (e) => {
this.setState({
titleText: e.target.value,
inputText: e.target.value,
});
};
handleSubmit = (e) => {
this.setState({
titleText: 'React is so awesome!',
inputText: '',
});
e.preventDefault();
};
render() {
const { inputText, titleText: dynamicTitleText } = this.state;
return (
<Fragment>
<h1>
{dynamicTitleText === ''
? 'Start typing for a dynamic title!'
: dynamicTitleText}
</h1>
<form onSubmit={this.handleSubmit}>
<button onClick={this.handleClick} type="button">
Click me to try more dynamic titles!
</button>
<input
value={inputText}
type="text"
placeholder="Enter some text to change title!"
onChange={this.handleChange}
/>
</form>
</Fragment>
);
}
}
export default EventAndState;
A few things to note from the above example component:
- Any change in state will cause the
EventAndState
component to render itself again (unless we explicitly tell it not to, as we will see is a possibility when looking at the component lifecycle). Since the state changes in this component on every single keystroke in theinput
field, it renders however frequently the user types (as well as when thebutton
is clicked or theform
submitted). More to the point: Every time our component rerenders, React will compare the old virtual DOM with the new virtual DOM; in this case, if a keystroke has been entered, then there will be a difference between the old virtual DOM and the new virtual DOM, thus resulting in a change being reflected in the actual DOM. - Worth repeating: Any time state changes in a React component,
render
will be called again. Every time. Unless we force it to not render. - In this example, for our
button
, we need to specifytype='button'
. Why? Because, as this post observes, buttons like<button>Click to do something</button>
are submit buttons. We can settype='button'
on thebutton
in order to change this. - In the
handleSubmit
method, we must calle.preventDefault()
in order to prevent theform
from submitting and the browser trying to move the page to something else that doesn't exist (basically resulting in a worthless refreshing of the page since we don't specify where the browser should go upon form submission).
This is awesome! We are beginning to see the power of state. We have it all in one place. We essentially have one source of truth for the component. We can update the UI very very efficiently using the same events we are used to but simply allow React to change the variables and then manage the rerender process down below.
Side note: Eventually, you will find that you want to handle onChange
for several input
s, and you'll notice you're basically rewriting the same code over and over, a giveaway that you could possibly do something more efficient. Without going into much detail here, this brief post on Medium discusses easy data binding with a generic onChange
hander:
onChange = e => this.setState({ [e.target.name]: e.target.value });
See the post for more details, but this can be pretty handy on a number of occasions. Worth noting is that this is certainly not a catch-all solution as it currently stands: it won't work if, say, you need to validate the format of an email address, check the strength of a password, etc. In those cases, you'll clearly need to run some conditional logic inside of setState
to handle those edge cases appropriately.
Practicing with managing state with events
Here were the original assignment instructions:
Assignment instructions
Make a new Component called StatePractice
- import react and Component
- make it a class, specifically a subclass of Component
- export the class as default
- add the constructor and super
- initialize state in the constructor with a property of message and a value of empty string
- add a render method
- return a sanity check
Add StatePractice to app.js
- go to app.js and import the component (remove EventAndState)
- render the component in app.js
Add event and state change to StatePractice
- remove sanity check and replace it with an input box and an h3
- in the h3, render the message piece of state
- use onFocus on the input box so that when the user clicks on the input, state updates
- setState on "message" to notify the user that they agree to the site terms of service by filling out the form
Add another event
- add the onMouseEnter event to the h3 so that it clears the text when the user hovers over it
Add another tag and event
- add an image tag (point at any URL)
- add another property to your state variable in the constructor called imageWidth and init to an empty string
- use the onLoad event to grab the image width
- if the image width is greater than 100px, then console.log("Your image is big!")
Extension
We can extend on this basic assignment to create the following behavior (disregard the top and bottom bars blinking and changing colors--that's due to the console being open during the demo):
If you try to mimic the behavior above, there are a few things worth noting:
- The
textarea
field should dynamically have the same width as the image. - You should actually
alert
the user upon first and only first focus of the text area. - As the docs note, the syntax for
setState
issetState(stateChange[, callback])
. We know a fair amount about thestateChange
required argument which can be a regular object (which is good to use when new state does not depend on previous state) or a callback (that automatically receives previous state and current props as available arguments). ButsetState
also accepts an optional callback function that will be executed oncesetState
is completed and the component is re-rendered. Generally, the React recommends usingcomponentDidUpdate()
(a lifecycle method we will get to before long) for such logic instead. - As the docs note about refs (in the component code below a single ref is used in order to send focus back to the
textarea
once a user has hovered over the title): Refs provide a way to access DOM nodes or React elements created in the render method: "In the typical React dataflow, props are the only way that parent components interact with their children. To modify a child, you re-render it with new props. However, there are a few cases where you need to imperatively modify a child outside of the typical dataflow. The child to be modified could be an instance of a React component, or it could be a DOM element. For both of these cases, React provides an escape hatch." They go on to recommend different use cases for refs: Managing focus (as is done in the component below), text selection, or media playback; triggering imperative animations; integrating with third-party DOM libraries. - There's also some in-line styling. We'll get to more styling possibilities later.
import React, { Component } from 'react';
class StatePractice extends Component {
constructor(props) {
super(props);
this.state = {
message: 'You agree to our terms of service by filling out the form.',
dynamicTitle: '',
inputText: '',
imageWidth: '',
termsAcknowledged: false,
};
this.textInput = React.createRef();
}
handleMouseEnter = (e) => {
this.setState(
{
dynamicTitle: 'New dynamic title inbound!',
inputText: '',
},
() => {
this.textInput.current.focus();
}
);
};
handleFocus = (e) => {
if (!this.state.termsAcknowledged) {
alert(this.state.message);
}
this.setState((prevState, props) => ({
termsAcknowledged:
prevState.termsAcknowledged || !prevState.termsAcknowledged,
}));
};
handleChange = (e) => {
this.setState({
dynamicTitle: e.target.value,
inputText: e.target.value,
});
};
handleImgLoad = (e) => {
this.setState(
{
imageWidth: e.target.width,
},
() =>
this.state.imageWidth > 100
? console.log(`Your image is big! (${this.state.imageWidth}px)`)
: null
);
};
handleFormSubmit = (e) => {
e.preventDefault();
this.setState({
dynamicTitle: 'Thanks for submitting! Try a new one!',
inputText: '',
});
};
render() {
const { inputText, dynamicTitle, imageWidth } = this.state;
return (
<form action="" onSubmit={this.handleFormSubmit}>
<h3 onMouseEnter={this.handleMouseEnter}>
{dynamicTitle === ''
? 'Type a message to begin! (or hover over this title at any time to begin a new one)'
: dynamicTitle}
</h3>
<button style={{ cursor: 'pointer' }}>Click to submit!</button>
<br />
<textarea
style={
imageWidth === ''
? { display: 'none' }
: { width: imageWidth + 'px', margin: 10 }
}
type="text"
value={inputText}
onChange={this.handleChange}
onFocus={this.handleFocus}
placeholder="Type new message here"
ref={this.textInput}
/>
<br />
<img src="binary-people.jpg" alt="" onLoad={this.handleImgLoad} />
</form>
);
}
}
export default StatePractice;
State and props together
One thing worth recalling is that we often want to pass data as props to components instead of adding file require
ment dependencies within a component. For example, if we have card data in a cards.js
file, then it's often architecturally better to require
that file at a high level (e.g. in App.js
) and pass the card data as props to a CardSet
component like so: <CardSet cards={cards} />
. This allows us to make sure CardSet
is modular as it can be. It does not depend on reading in data from anywhere else. It simply uses the data it is given (as props). Another nice reason about passing data as props is that props are immutable. So whatever is passed as cards
to CardSet
will not change or, if it does, it will only change at a level above CardSet
. So the CardSet
does not have to worry about the cards
data changing--that is someone else's problem.
Something worth noting concerning situations when you want to get information from a certain element you created when mapping through data is that what you use as a callback for the JSX should often be an anonymous function where you can pass the function being returned data about the specific element. So instead of something like
<button onClick={this.saveCourse}>Save</button>
where whatever saveCourse
is it just gets called (we aren't passing the saveCourse
method anything specific about the element on which there is a listener), we instead can pass data to saveCourse
by means of an anonymous function:
<button onClick={(event) => this.saveCourse(event, index, dataOne, dataTwo, ...)}>Save</button>
The result is you can define your methods in such a way that they are waiting for specific data instead of only being passed events:
saveCourse = (event, index, dataOne, dataTwo, ...) => {
console.log(index)
}
Note the ...
above refers to whatever else might be passed to saveCourse
by means of the anonymous callback in our JSX. The point of this is that if you want to pass data to an event handler beyond just the event itself, then you will need to use something like the following (important to note below is that for the anonymous function () => this.handleEvent
you must provide the event as the argument to the anonymous function if you want to use it in your event handler; that is, if you want to make use of the event inside of this.handleEvent
while also passing data to this.handleEvent
, then your JSX callback will need to look like (event) => this.handleEvent(event, dataToBePassed)
instead of () => this.handleEvent(event, dataToBePassed)
because in the latter case you actually don't have access to the event
object):
<element onEvent={(event) => this.handleEvent(event, dataOne, dataTwo, ...)}> </element>
as opposed to the "normal"
<element onEvent={this.handleEvent}> </element>
because this "normal" way results in not passing anything to the handleEvent
method other than the event itself. We definitely cannot do something like
<element onEvent={this.handleEvent(dateOne, dataTwo, ...)}> </element>
because this will result in this function being called immediately on render which is not what we want at all. This should be very empowering because now we have the power to generate a bunch of elements and pull data from these elements based on how the user interacts with them. And we can setState
within these methods using the data passed to it as a result of the user's interaction with the UI.
Stateless componenets vs stateful components (terminology)
Let's get our terms down for "stateless" and "stateful". What do these terms really mean?
- Stateful:
- Has state. In React-speak, that means a component is managing some kind of state variable. There's data inside of the component that's changing, and the component is in charge of that.
- A component will have state if it is managing its own data.
- The consequence of managing state is that given input
a
in a component, it will not always equal outputb
. The point is that if you have a stateful component and you give it inputa
, then you cannot guarantee that the output is always going to beb
. - Becuase the internal data is changing in a stateful component, if you give it
a
you might getb
as the output one time, but you might getc
another time,d
another time, etc. - Essentially: A stateful component can be thought of as complex and smart.
- Stateless:
- Does not have state.
- The consequence of not managing state is that input
a
will always equal outputb
. - If you give a stateless component a piece of input or data, then you will always get the same output for that specific input. Nothing is changing internally.
- Essentially: Simple, pure, presentational
So a stateless component's job is simply to represent some HTML. So if you find yourself writing a bunch of markup in a stateful component, then odds may be decent you should abstract some of that markup away and create a new strictly presentational component and fill in the presentational component by passing data down to it as props.
The Component Lifecycle and HTTP
server-side rendering vs. what we typically aim to do with React
When we go to Wikipedia, we get the DOM for some page. When we go to another page on Wikipedia, we make an HTTP request and we get a whole new DOM. The server accepts our request and repackages a bunch of HTML, CSS, and JS which the browser can understand and then ships that to us. Whenever we browse to a new page, this corresponds to a new request to the server and a subsequent shipment of HTML, CSS, and JS. Basically: new page request via HTTP -> server fetches appropriate HTML, CSS, and JS -> server responds with HTML, CSS, and JS which the browser parses and returns as a whole new DOM
.
We aim to change such a cycle with React. React is "back-end agnostic." Often one will be using Node.js and Express, but other options are certainly possible.
The difference between server-side rendering and what React aims to do is that when we need any new data at all, we still will make another HTTP request, but we will not get back HTML, CSS, and JS. Instead, we will get back JSON. And this will happen over and over and over. You can get back other things (XML, other files, etc.) but by far the most common is going to be JSON.
And that is the route we are going to take. Instead of loading up a new DOM, it's going to be
HTTP -> JSON : HTTP -> JSON : HTTP -> JSON ...
So React can update itself. It's purely a UI library so it has no idea how the data was made or who is on the other end. It could be PHP, Node, Go, C++, etc. All React knows is that it got some data and that it can do something with it. The server's job is just going to be to kick back some JSON.
Instead of server-side rendering, where the server does everything (like where you use templating engines to build the UI with the server and couple the logic), we are now creating a clean separation between front-end and back-end. This separation now means that there needs to be some chatter between the front-end and the back-end (i.e., chatter between the front-end, React, and the server that is going to provide the data, the back-end). To manage this chatter as well as other things, React gives us the component lifecycle which will help us manage how and when things should be happening.
HTTP requests using React
Up until now, everything we have done has been from hard-coded data. We loaded static files into our application directory and used the data from those files. But that's not really how the web works anymore. Typically, React is going to be asking for data from somewhere else whether it be your own server or someone else's because React is just a UI framework. It doesn't have any access to databases or anything like that. So generally we will be making HTTP requests to a server in order to get data. In order to make HTTP requests, however, we need an HTTP client.
Unlike Angular or some other frameworks you may have worked with, there isn't a built-in HTTP client with React. So we can use the fetch API which is built into native JavaScript now. There's also tested and tried jQuery where you can use $.ajax
. We are going to use axios though. Why? The reason (among others) is that it is the most commonly used HTTP client in production.
Note about environment variables and hiding secrets (e.g., API keys) in React
This still needs to be addressed (some ideas included below for time being)
In something like Express/Node (especially in the context of server-side rendering), you might often put API keys and the like into an .env
file and use dotenv to access your environment variables throughout your application. Sense everything in React is done client- or browser-side, we have to be more careful with secrets like API keys.
The docs indicate that environment variables are embedded during the build time. It seems like most solutions involve sending a request to the backend where environment variables can remain hidden and then having the backend make the request for us (subsequently sending what response the backend request gets to the frontend).
Assuming we are using Express as the backend (I imagine it would work similarly otherwise as well), at least one approach I have seen involves having the client make a call to, say, /api
, and then have an /api
route in Express proxy that request to the actual API URL:
app.use('/api', (req, res) => {
const method = req.method.toLowerCase();
const headers = req.headers;
const url = 'your_actual_api_url';
// Proxy request
const proxyRequest = req.pipe(
request({
url
headers,
method,
})
);
const data = [];
proxyRequest.on('data', (chunk) => {
data.push(chunk);
});
proxyRequest.on('end', () => {
const { response } = proxyRequest;
const buf = Buffer.concat(data).toString();
res.status(response.statusCode).send(buf);
});
});
And I have also seen another approach that looks a bit more elaborate (but possibly more effective/secure?).
This is something to spend a fair amount of time with to ensure secrets aren't exposed in the bundle for a real production application.
The component lifecycle
Asynchronous Rendering Quandary: Forced Introduction to the Component Lifecycle
We have an example of how one API works (open weather map) and how to get some data from it:
import React from 'react';
import './App.css';
import axios from 'axios';
function App() {
const url = 'https://api.openweathermap.org/data/2.5/weather?q=London&units=imperial&appid=e312dbeb8840e51f92334498a261ca1d'
let weatherData= axios.get(url).then(resp => console.log(resp.data))
return (
<div className="App">
<h1>Sanity Check</h1>
</div>
);
}
export default App;
The question now is how do we actually use the data we get back from the API we send a request to? Because this is certainly going to be the most common way in which you fetch data in React (or really any UI framework). Essentially: How do we get the data from our API call into our JSX that we actually want to render to the screen?
The key thing to remember here is that HTTP requests are asynchronous, meaning JavaScript will not wait for the response. It will keep on moving. So above, the const url = ...
line will run and the next line will start to run, but JavaScript will keep going and return the JSX without the data even if we don't intend for or want it to. So we are going to want to use state. State is meant for this very thing when we have data that needs to change. We start off with our basic rendering without data, but that's not what we ultimately want in the DOM. We are going to want to change the DOM once we hear back from our API request. So this is a very good example of when we would want to use state. So for the time being (until we get to hooks), we will convert App
to be a class.
The following may be one attempt at trying to come up with a solution to our apparent problem:
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
const url = 'https://api.openweathermap.org/data/2.5/weather?q=London&units=imperial&appid=e312dbeb8840e51f92334498a261ca1d'
let weatherData = axios.get(url).then(resp => {
this.state = {
temp: resp.data.main.temp
}
})
}
render() {
return (
<div className="App">
<h1>{this.state.temp}</h1>
</div>
);
}
}
export default App;
Why won't this work? Again, it's the asynchronous part at work here. this.state
in the axios
request doesn't get run until after the render
runs (we also can't be assigning a variable inside of an axios
request that we can nakedly access later). So there are all sorts of things wrong with this.
So where do we put our request? We have an asynchronous thing that needs to happen. We can't put it in the constructor. So what if we put our request in the render
like so:
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
this.state = {
temp: ''
}
}
render() {
const url = 'https://api.openweathermap.org/data/2.5/weather?q=London&units=imperial&appid=e312dbeb8840e51f92334498a261ca1d';
axios.get(url).then(resp => {
this.setState({
temp: resp.data.main.temp
})
})
return (
<div className="App">
<h1>{this.state.temp}</h1>
</div>
);
}
}
export default App;
If you run the code above, then it looks like it works (you will get a number on the screen with the data we retrieved), but this is really really bad. Why? Because we are updating state inside of render
. And what happens when we update state? Our component renders again. React is smart enough to see what we are doing and keeps us from hurting ourselves. So our request doesn't belong in render
either. If React didn't have safety mechanisms in place, then we would get caught in an infinite loop: our setState
would run, cause a render, which would then update state, cause another render, etc., etc.
So where on earth does our request belong then? This is where the lifecycle methods for components become crucial! We will make use of the componentDidMount
lifecycle method like so:
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
this.state = {
temp: ''
}
}
componentDidMount() {
const url = 'https://api.openweathermap.org/data/2.5/weather?q=London&units=imperial&appid=e312dbeb8840e51f92334498a261ca1d';
axios.get(url).then(resp => {
this.setState({
temp: resp.data.main.temp
})
})
}
render() {
return (
<div className="App">
<h1>{this.state.temp}</h1>
</div>
);
}
}
export default App;
So we get exactly the same result now, but what gives? We didn't call componentDidMount
anywhere inside render
or anywhere else. So why did our request run, our state update, and then our component render again with the data we wanted? This is the lifecycle of the component at work.
At the very beginning of our component we have a class App
which has all the goodness that comes from being a Component
which react
gives us. Because App
is a component and we called super
within our class definition, we get all that cool stuff that comes with being a React component, namely all of the lifecycle methods for React components. In particular, one of the lifecycle methods we get is componentDidMount
, and React will look for the existence of this method and, if it exists, React will call this method automatically after the first render. Without even saying else just yet, you should be able to see why it is often advised to put asynchronous requests in componentDidMount
: React will call this method after the initial render of your component and you can do all your fun data retrieval then. Since you will still render a component without your data at first, the idea of "conditional rendering" is prominent. You may see stuff like a "loading" component that will display if our data hasn't been retrieved just yet. As soon as we get our data, then we can render what we actually want.
Initial Rendering Order
Consider the following pseudocode:
class Foo extends Component {
constructor() {
super();
this.state = {temp: ''}
}
componentDidMount() {
axios
...
}
render() {
...
return(
...
)
}
}
In what order does all of this execute?
- The very first thing that will happen when an instance of the
Foo
class is created is itsconstructor
will run (which, in this case, means callingsuper
and inhereting everything that comes with being a ReactComponent
as well as setting initial values ofstate
, etc.). - The
render
method is called and whatever is being returned is placed in the virtual DOM and then the actual DOM (on the first rendering of the application). componentDidMount
, if it exists (which it does above!), will get called after the initialrender
. The reason this is a sensible method is because of the single-threaded nature of JS. Anything asynchronous gets plucked out of the event loop and plopped onto the call stack. This method is where it makes sense to make HTTP requests usingaxios
,fetch
, or something similar. The idea is then to callsetState
once you have gotten the data back that you want.- Calling
setState
results in a rerender (a new render results any time state changes unless we force different behavior). The idea is to get one version of the DOM upon the initial render without the desired data (along with probably a loading screen of some sort). Once the desired data is obtained and the state updated, the state can then be used to render the DOM how we want.
Rendering Process (mounting, updating, unmounting)
Here is the simplified rendering process for components in React:
Mounting | Updating | Unmounting |
---|---|---|
1. constructor | New props / new state | |
2. render | 4. render again (as many times as needed) | |
3. componentDidMount | 5. componentDidUpdate | componentWillUnmount |
Every React component has to mount, where "mount" is simply the term for "it wasn't in the virtual DOM a second ago and I'm about to put it in there"; that is, the entire component is being rendered.
Here is a high-level overview of the whole process:
- Mounting: The very first thing that happens is the
constructor
runs when the component is actually created or called by something else. Soindex.js
starts everything withReactDOM.render(<App />, document.getElementById('root'));
which asks forApp
fromApp.js
. WhenApp
gets asked for, theconstructor
forApp
will run, and then therender
forApp
will run. And once the firstrender
is finished,componentDidMount
will run (if it exists). And at this point our React component is just sitting there. It doesn't have anything else to do. After that, we get into the updating phase. - Updating: The mounting phase is now over and the component is in the DOM. Thus begins the update phase. If we get new
props
or newstate
, thenrender
will run again; as soon asrender
is finished running,componentDidUpdate
will run again. And then we get newprops
. Thenrender
runs again. ThencomponentDidUpdate
runs again. And so on and so forth so long as newprops
or newstate
come in. Hence, in a nutshell, the update phase is a circular process that looks as follows:- new
props
or newstate
render
componentDidUpdate
This process happens again and again and again (however long is necessary). Every time there is newprops
or newstate
, the component will rerender andcomponentDidUpdate
will always follow. We needrender
to run because we need the component to update the virtual DOM. AndcomponentWillUpdate
will allow us to run logic after we have an update--we might need to know if something happened.
- new
- Unmounting: The unmounting process is when the component is going to get removed from the DOM. So, in this example, it may be that we do not need
App
anymore because we are going to go to something else.componentWillUnmount
runs as a last gasp right before it's actually removed. So if we have logic we need to run, if we need to clear up something in memory or we want to alert some other component or some other part of the application, then we can do that inside ofcomponentWillUnmount
.
The above is the lifecycle that components go through. There are a couple of other methods, but these are the most frequently used. The point is that the power of the component lifecycle is that it gives us the ability to patch into the process but let React manage how and when everything happens. We can use the methods React is giving us to optimize how everything is being processed and rendered.
Component Lifecycle Visualized (and legacy lifecycle methods addressed)
It's worth taking a good look at the docs concerning React.Component
. In particular, this blog post on React's official site addresses some updates concerning asynchronous rendering (this talk by Dan Abramov addresses what the React team has been working on). Essentially, the React team noted that some of their legacy component lifecycles tended to encourage unsafe coding practices:
componentWillMount
componentWillReceiveProps
componentWillUpdate
They show in their Gradual Migration Path that React 17.0+ (as of writing now, April 28, 2020, the latest React version is 16.13.1) will actually remove the lifecycle methods listed above. This Reddit post points out the general approach to making sure you avoid the methods listed above:
- Use
componentDidMount
instead ofcomponentWillMount
- Use
static getDerivedStateFromProps
instead ofcomponentWillReceiveProps
(only if you really need to derive state; the React team has another blog post literally titled You Probably Don't Need Derived State) - Use
componentDidUpdate
instead ofcomponentWillUpdate
With all of the above said, this site (which React actually links to in the docs) illustrates the commonly used lifecycle methods we have just mentioned in passing:
They also show the less common lifecycles as well (although it's a more cluttered picture):
The following link list may be helpful for reference in the context of the outline(s) above:
- Mounting:
- Updating:
- Unmounting:
This article on Medium explores all of the lifecycle methods listed above in the hypothetical context of building a music player.
Reference: The docs on React.Component
(in a nutshell)
See React.Component in the docs for all the gory details. Below is a modest attempt to provide just a nuts and bolts reference for ease of use (i.e., consult the actual docs for examples of everything below).
Each component has several "lifecycle methods" that you can override to run code at particular times in the process. You can use this lifecycle diagram as a cheat sheet. In the list below, commonly used lifecycle methods are marked as bold. The rest of them exist for relatively rare use cases.
- Mounting: These methods are called in the following order when an instance of a component is being created and inserted into the DOM:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
- Note: These methods are considered legacy and you should avoid them in new code:
- Updating: An update can be caused by changes to props or state. These methods are called in the following order when a component is being re-rendered:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate() These methods are considered legacy and you should avoid them in new code:
- Note: These methods are considered legacy and you should avoid them in new code:
- Unmounting: This method is called when a component is being removed from the DOM:
- Error Handling: These methods are called when there is an error during rendering, in a lifecycle method, or in the constructor of any child component.
- Other APIs: Each component also provides some other APIs:
- Class Properties
- Instance Properties
The Lifecycle: componentDidMount()
We will now explore componentDidMount() a bit more. As the docs note: componentDidMount()
is invoked immediately after a component is mounted (inserted into the tree). Initialization that requires DOM nodes should go here.
Example: If you need something to already exist in the DOM, then this is where you can access that thing. You could then technically use element.addEventListener
or something similar even though we will rarely use that. The point is that if you need an actual DOM element to grab after rendering, then this is a place to do that.
If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
Example: This is a good place to instantiate HTTP requests!
We are now going to consider a simple use case of componentDidMount()
in the context of trying to use the Materialize framework. We can copy their CDN links and dump them into our index.html
in our public
folder (under title
or somewhere appropriate in the head
):
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
Suppose we like the Modals in Materialize and we want to use one in our React application (there are lots of modules for React modals and other such things, but we're just going to use Materialize here to illustrate how componentDidMount()
can be used in this instance). We can copy their base modal:
<!-- Modal Trigger -->
<a class="waves-effect waves-light btn modal-trigger" href="#modal1">Modal</a>
<!-- Modal Structure -->
<div id="modal1" class="modal">
<div class="modal-content">
<h4>Modal Header</h4>
<p>A bunch of text</p>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Agree</a>
</div>
</div>
Note that we will need to change class
to className
to make this work in our application (we will also need get rid of the HTML comments or change them to be {/* JSX Comments */}
). The Materialize docs note that the following about initialization and opening a modal using a trigger:
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.modal');
var instances = M.Modal.init(elems, options);
});
Our modal uses a trigger. But notice the use of document.querySelectorAll('.modal')
. What does this assume? That an element with a class of modal
actually exists in the DOM (they have even structured things to make sure that element exists by only adding the event listener once all of the DOM content has loaded). In our situation, in the context of React, how can we add an event listener to an element if the element doesn't even exist in the DOM on the first go around? This is exactly where componentDidMount()
comes into play! The React team even told us as much in the docs: "Initialization that requires DOM nodes should go here." Materialize tells us how to initialize a modal to open using a trigger, but they gave us a vanilla JavaScript approach--we want the React approach, and the sensible way of doing this in React is to perform the initialization in the componentDidMount()
lifecycle method. One last potential "gotcha" in this case concerns the fact that we have to be careful of stuff that we bring in from the outside: right now, with just
var elems = document.querySelectorAll('.modal');
var instances = M.Modal.init(elems, options);
in componentDidMount()
we would get an error: 'M' is not defined
. Basically, M
was inserted by the Materialize script (you can check this via console.log(M)
), so M
does exist, but it's attached to the window
object (i.e., console.log(window.M)
will give the same result as console.log(M)
because we are in the window when we are in the browser console). In our React application, we are not in the window
. So we need window.M.Modal.init(elems)
(we drop the options
since we aren't going to use those here). All in all, our component might look something like the following:
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
this.state = {
temp: ''
}
}
componentDidMount() {
const url = 'https://api.openweathermap.org/data/2.5/weather?q=London&units=imperial&appid=e312dbeb8840e51f92334498a261ca1d';
axios.get(url).then(resp => {
this.setState({
temp: resp.data.main.temp
})
})
const elems = document.querySelectorAll('.modal');
const instances = window.M.Modal.init(elems);
}
render() {
return (
<div className="App">
<h1>{this.state.temp}</h1>
<a className="waves-effect waves-light btn modal-trigger" href="#modal1">Modal</a>
<div id="modal1" className="modal">
<div className="modal-content">
<h4>Modal Header</h4>
<p>A bunch of text</p>
</div>
<div className="modal-footer">
<a href="#!" className="modal-close waves-effect waves-green btn-flat">Agree</a>
</div>
</div>
</div>
);
}
}
export default App;
The Lifecycle: render()
We know a good deal about render()
as we have interacted with it a fair amount so far, but we can still glean some useful information from the docs (and also recall that the render()
method is the only required method in a class component):
- When called,
render()
should examinethis.props
andthis.state
and return one of the following types: React elements, arrays and fragments, portals, strings and numbers, booleans or null.- Recall that all of our "regular JavaScript" happens in
render()
above thereturn
. Typically, this means we might destructure things we want to use off ofthis.props
and/orthis.state
, mash up our data however we see fit, and then use the mashed up data within thereturn
.
- Recall that all of our "regular JavaScript" happens in
- The
render()
function should be pure, meaning that it does not modify component state, it returns the same result each time it's invoked, and it does not directly interact with the browser.- Concerning state, the docs basically communicate that we should not call
setState
inrender()
. If you are doing that, then you have likely organized your component incorrectly. Concerning the browser,render()
's job is to just build the virtual DOM appropriately, not interact with the actual DOM in some way. It isReactDOM
's job to interact with the browser and the actual DOM (hence its name, ReactDOM).
- Concerning state, the docs basically communicate that we should not call
- If you need to interact with the browser, perform your work in
componentDidMount()
or the other lifecycle methods instead. Keepingrender()
pure makes components easier to think about.- Note how this corresponds to what we just did with the Materialize modal (where we needed to access an element with class
modal
that did not yet exist in the DOM): we needed to wait for the firstrender()
before we could access the element.
- Note how this corresponds to what we just did with the Materialize modal (where we needed to access an element with class
The bottom line in all of this is to simply keep render()
pure. It should always return the same thing given the same input. That doesn't mean don't use state
in render (we will do that a lot!). It just means do not set state
or do not mutate state
inside of render()
. That and any time state
or props changes the render method will fire. So we will get that method firing any time one of those two things changes.
The Lifecycle: componentDidUpdate()
We are now going to take a look at componentDidUpdate(). As they note: "componentDidUpdate()
is invoked immediately after updating occurs. This method is not called for the initial render." So componentDidMount()
runs the very first time after the initial render. And componentDidUpdate()
runs after every render thereafter. So if you make an HTTP request in componentDidMount()
and get the data and set new state with this data, then render()
will run again because new state was set, and this second render()
(and every one thereafter) will result in componentDidUpdate()
being called. Here is the chain of events:
Order | Method | Description |
---|---|---|
1 | constructor() | constructor fires when creating component |
2 | render() | initial render (no async data loaded/available) |
3 | componentDidMount() | component has mounted (make network requests here) |
4 | render() | state has changed (we updated state from data obtained in componentDidMount ) |
5 | componentDidUpdate() | component has been updated (actual DOM now has data we wanted from network request) |
6 | render() | user does something in UI to trigger new state/props for this component |
7 | componentDidUpdate() | component has been updated/rerendered to reflect new state/props |
8 | render() | user does something in UI to trigger new state/props for this component |
9 | componentDidUpdate() | component has been updated/rerendered to reflect new state/props |
... | ... | ... |
So what gives? Why would we ever want to use componentDidUpdate
? Mostly because what componentDidUpdate
receives: componentDidUpdate(prevProps, prevState, snapshot)
. That is, yes, it probably will not make much sense to call componentDidUpdate
if you do not care about how state or props updated and compares to previous (i.e., before the update) state or props.
Essentially componentDidUpdate
will give you the ability to look at what was (via prevProps
and prevState
) and what is (via current props, this.props
, and current state, this.state
) in order to decide what needs to happen in your application.
Lastly, what is snapshot
? As the docs note at the bottom of the entry on componentDidUpdate
: "If your component implements the getSnapshotBeforeUpdate()
lifecycle (which is rare), the value it returns will be passed as a third “snapshot” parameter to componentDidUpdate()
. Otherwise this parameter will be undefined." Let's take a look at the docs on getSnapshotBeforeUpdate()
: "getSnapshotBeforeUpdate()
is invoked right before the most recently rendered output is committed to e.g. the DOM. It enables your component to capture some information from the DOM (e.g. scroll position) before it is potentially changed. Any value returned by this lifecycle will be passed as a parameter to componentDidUpdate()
. This use case is not common, but it may occur in UIs like a chat thread that need to handle scroll position in a special way. A snapshot value (or null
) should be returned."
We will hardly ever use getSnapshotBeforeUpdate()
, but it's useful to know that it exists. The componentDidUpdate()
lifecycle method will be especially useful when we get React router.
Continuing, suppose we wanted to do something like the following:
componentDidUpdate(prevProps, prevState) {
const isRaining = this.state.weather.includes('rain');
if (isRaining) {
this.setState({
isRaining: 'Rain rain go away!'
});
}
}
If it is raining in the city you just received data about, then you just might be the recipient of a really ugly warning:
Error: Maximum update depth exceeded. This can happen when a component
repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
React limits the number of nested updates to prevent infinite loops.
We are not using componentWillUpdate
so it seems we at least know the likely culprit: How are we repeatedly calling setState
inside of componentDidUpdate
though? What happened? This example illustrates why we need prevProps
and prevState
in componentDidUpdate
. Before examining our specific example, let's see what the docs say about this: "You may call setState()
immediately in componentDidUpdate()
but note that it must be wrapped in a condition like in the example above, or you'll cause an infinite loop." And they give the following example:
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
With this in mind, can we now deduce what the issue is with the code we originally had for our own example? It may be most helpful to look at the entire lifecycle in sequence to see why the sequence will never end:
constructor
runs- initial
render
runs componentDidMount
runs (we get our default data)render
runs (user searches for a city where it is raining and state is set with new city data)componentDidUpdate
runs (and state is set)render
runs (to reflect new state fromcomponentDidUpdate
above)componentDidUpdate
runs (because we just updated the DOM; state is set again since it is still raining in the searched for city (i.e., we never changedisRaining
for the searched for city))render
runs (to reflect new state (even though it really isn't new) fromcomponentDidUpdate
above)...
Hopefully the problem above is clear. We need some way of making sure that we set the state only when we need to within componentDidUpdate
. And the way we can do this is by comparing previous props/state (i.e., prevProps/prevState
) with current props/state (i.e., this.props/this.state
). Specifically, we could do something like the following:
componentDidUpdate(prevProps, prevState) {
if (this.state.weather !== prevState.weather) {
const isRaining = this.state.weather.includes('rain');
if (isRaining) {
this.setState({
isRaining: 'Rain rain go away!'
});
} else {
this.setState({
isRaining: ''
});
}
}
}
Hence, we will only set state again (raining or not) if the city weather data we have received after a render is different from the data we received before the render that triggered componentDidUpdate
.
Perhaps more clearly: The bottom line is that you need to always conditionally set state within componentDidUpdate
; otherwise, setting state in componentDidUpdate
triggers a render, which then triggers componentDidUpdate
which again triggers render because state was set (even if we were setting the state to be the exact same), etc., etc.
The Lifecycle: componentWillUnmount()
As the docs note: "componentWillUnmount()
is invoked immediately before a component is unmounted and destroyed (i.e., before it is removed from the DOM). Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount()
. You should not call setState()
in componentWillUnmount()
because the component will never be re-rendered. Once a component instance is unmounted, it will never be mounted again."
In our weather application, how could we possibly make use of componentWillUnmount()
? What if we selectively got rid of the weather data modal and then brought it back somehow (and somehow tied a timer to this somehow as well)? Conditional rendering is one possible case where a component may be mounted one moment and umounted/destroyed the next (i.e., it may be mounted again later but only as a brand new component ... the unmounted one never comes back). So this is a very useful hook for cleanup that you need to do. So if you have a heavy workload or series of network requests in the background that need to stop, then this will be the place to do that.
Practicing what we know so far: city weather application
Using what we know so far about the different component lifecycle methods, we can make a reasonably decent, however basic, city weather application. I have expanded on some of the notes included so far. You can go to the repository and do an npm install
and don't forget to create an .env
file with REACT_APP_WEATHER_APP=e312dbeb8840e51f92334498a261ca1d
in it or you can go to the live site to play around with it (demo below):
There are four files that serve as the core of the application:
1. App.js
2. Headers.jsx
3. Modal.jsx
4. UnitedStatesSelectionForm.jsx
Click to see App.js
contents here
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
import moment from 'moment';
import Headers from './Headers';
import Modal from './Modal';
import UnitedStatesSelectionForm from './UnitedStatesSelectionForm';
class App extends Component {
constructor(props) {
super(props);
this.state = {
searchCount: -1,
genWeatherDesc: '',
specificWeatherDesc: '',
temp: '',
feelsLike: '',
low: '',
high: '',
cityId: '',
cityName: '',
cityState: '',
timezone: '',
sunrise: '',
sunset: '',
icon: '',
iconURL: '',
showCityDetailsModal: true,
sameCityState: false,
persistentTimeCount: 0
}
}
componentDidUpdate(prevProps, prevState){
if (this.state.cityId !== prevState.cityId) {
this.setState((prevState, props) => ({
searchCount: prevState.searchCount + 1
}));
}
}
componentDidMount() {
this.persistentTimer = setInterval(() => {
this.setState((prevState, props) => ({
persistentTimeCount: prevState.persistentTimeCount + 1
}));
}, 1000);
this.getCityWeather('Nashville', 'TN')
}
searchCity = e => {
e.preventDefault();
const city = document.getElementById('city').value;
const cityState = document.getElementById('city-state').value;
this.getCityWeather(city, cityState);
}
getCityWeather = async (city, cityState) => {
if (city === this.state.cityName && cityState === this.state.cityState) {
alert(`You already have data for ${city}, ${cityState}!`);
return;
}
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city},${cityState},us&units=imperial&appid=${process.env.REACT_APP_WEATHER_APP}`;
let weatherInfo;
try {
weatherInfo = await axios.get(url);
} catch (error) {
alert(`No data found for the city you entered: "${city}". Please try again!`);
return;
}
let { weather: weatherDesc, main: weatherFacts, name: cityName, sys: { sunrise, sunset }, timezone, id: cityId } = weatherInfo.data;
let { main: genWeatherDesc, description: specificWeatherDesc, icon } = weatherDesc[0];
let { temp, feels_like: feelsLike, temp_min: low, temp_max: high } = weatherFacts;
sunrise = moment.unix(sunrise).utcOffset(timezone / 60).format('h:mm A');
sunset = moment.unix(sunset).utcOffset(timezone / 60).format('h:mm A');
let iconURL = `http://openweathermap.org/img/wn/${icon}@2x.png`;
let desiredState = { genWeatherDesc, specificWeatherDesc, temp, feelsLike, low, high, cityName, icon, cityState, sunrise, sunset, iconURL, cityId };
this.setState((prevState, props) => ({
persistentTimeCount: 0,
sameCityState: false,
...desiredState
}));
}
toggleModal = e => {
this.setState((prevState, props) => ({
showCityDetailsModal: !prevState.showCityDetailsModal,
sameCityState: true
}));
}
render() {
console.log('render is running...')
const { genWeatherDesc, specificWeatherDesc, temp, feelsLike, low, high, cityName, cityState, sunrise, sunset, iconURL, showCityDetailsModal, searchCount, cityId, persistentTimeCount, sameCityState } = this.state;
const headerProps = {temp, specificWeatherDesc, cityName, cityState, searchCount};
const modalProps = {genWeatherDesc, specificWeatherDesc, temp, feelsLike, low, high, cityName, iconURL, cityState, sunrise, sunset, searchCount, cityId, persistentTimeCount, sameCityState};
return (
<div className="App">
<div className="row">
<div className="col s6 offset-s3">
<Headers { ...headerProps } />
<div className="row center-align">
{showCityDetailsModal ? <a
className="waves-effect waves-light btn modal-trigger"
href="#modal1"
style={{marginRight: 40 }}
>See Weather Details
</a> : ''}
<button style={{marginLeft: 20}} onClick={this.toggleModal} className='btn'> {showCityDetailsModal ? 'Remove Weather Details and Timer' : 'Add Weather Details and Timer'}</button>
</div>
<form onSubmit={this.searchCity}>
<UnitedStatesSelectionForm />
</form>
</div>
</div>
{showCityDetailsModal ? <Modal { ...modalProps } /> : ''}
</div>
);
}
}
export default App;
Click to see Headers.jsx
contents
import React, { Fragment } from 'react';
const Headers = ({temp, specificWeatherDesc, cityName, cityState, searchCount}) => {
return (
<Fragment>
{searchCount === 0 ? <h2>Search for US City Weather!</h2> : null}
{searchCount === 0 ? <h3 style={{marginBottom: 30}}>Sample City: {cityName}, {cityState}</h3>
: <h2>{cityName}, {cityState}: {Math.round(temp)}{String.fromCharCode(176, 70).toUpperCase()} and {specificWeatherDesc}</h2> }
</Fragment>
);
};
export default Headers;
Click to see Modal.jsx
contents
import React, { Component, Fragment } from 'react';
class Modal extends Component {
constructor(props) {
super(props);
this.state = {
timerCount: 0
};
}
componentDidMount() {
console.log('componentDidMount is running ...')
const modalElem = document.querySelectorAll('.modal');
const selectElem = document.querySelectorAll('select');
window.M.Modal.init(modalElem);
window.M.FormSelect.init(selectElem);
this.timer = setInterval(() => {
this.setState((prevState, props) => ({
timerCount: prevState.timerCount + 1
}));
}, 1000);
}
componentWillUnmount() {
console.log('Component is about to be history ... (compnentWillUnmount running ...)')
clearInterval(this.timer);
}
componentDidUpdate(prevState, prevProps) {
console.log('THIS IS YOUR PERSISTENT TIME COUNT: ', this.props.persistentTimeCount)
console.log('componentDidUpdate is running ... ')
if( this.props.cityId !== prevState.cityId ) {
this.setState({
timerCount: 0
});
}
}
render() {
console.log('render is running ...')
const {genWeatherDesc, specificWeatherDesc, temp, feelsLike, low, high, cityName, iconURL, cityState, sunrise, sunset, sameCityState, persistentTimeCount} = this.props;
const {timerCount} = this.state;
console.log(timerCount)
return (
<Fragment>
<h4>How long you have been viewing weather data for {cityName}, {cityState} (seconds): {sameCityState ? persistentTimeCount : timerCount}</h4>
<div id="modal1" className="modal" style={{ maxWidth: 700 }}>
<div className="modal-content">
<table className="striped bordered">
<thead>
<tr>
<th>Descriptor</th>
<th>Information</th>
</tr>
</thead>
<tbody>
<tr>
<td>City</td>
<td>
{cityName} ({cityState})
</td>
</tr>
<tr>
<td>Current Temperature</td>
<td>{Math.round(temp)} (℉)</td>
</tr>
<tr>
<td>Feels Like</td>
<td>{Math.round(feelsLike)} (℉)</td>
</tr>
<tr>
<td>High</td>
<td>{Math.round(high)} (℉)</td>
</tr>
<tr>
<td>Low</td>
<td>{Math.round(low)} (℉)</td>
</tr>
<tr>
<td>Sunrise</td>
<td>{sunrise}</td>
</tr>
<tr>
<td>Sunset</td>
<td>{sunset}</td>
</tr>
<tr>
<td>Weather Description</td>
<td>
{genWeatherDesc} ({specificWeatherDesc})
</td>
</tr>
<tr>
<td>Weather Condition</td>
<td>
<img src={iconURL} alt="" />
</td>
</tr>
</tbody>
</table>
</div>
<div className="modal-footer">
<a href="#!" className="modal-close waves-effect waves-green btn-flat">
Cool!
</a>
</div>
</div>
</Fragment>
);
}
}
export default Modal;
Click to see UnitedStatesSelection.jsx
contents
import React, { Fragment } from 'react';
const UnitedStatesSelectionForm = () => {
return (
<Fragment>
<div className="input-field col s7">
<input type="text" id="city" className="validate" required />
<label htmlFor="city">Enter a City for Weather Data</label>
</div>
<div className="input-field col s3">
<select id="city-state">
<option value="" disabled defaultValue>
US State
</option>
<option value="AL">Alabama</option>
<option value="AK">Alaska</option>
<option value="AZ">Arizona</option>
<option value="AR">Arkansas</option>
<option value="CA">California</option>
<option value="CO">Colorado</option>
<option value="CT">Connecticut</option>
<option value="DE">Delaware</option>
<option value="DC">District of Columbia</option>
<option value="FL">Florida</option>
<option value="GA">Georgia</option>
<option value="HI">Hawaii</option>
<option value="ID">Idaho</option>
<option value="IL">Illinois</option>
<option value="IN">Indiana</option>
<option value="IA">Iowa</option>
<option value="KS">Kansas</option>
<option value="KY">Kentucky</option>
<option value="LA">Louisiana</option>
<option value="ME">Maine</option>
<option value="MD">Maryland</option>
<option value="MA">Massachusetts</option>
<option value="MI">Michigan</option>
<option value="MN">Minnesota</option>
<option value="MS">Mississippi</option>
<option value="MO">Missouri</option>
<option value="MT">Montana</option>
<option value="NE">Nebraska</option>
<option value="NV">Nevada</option>
<option value="NH">New Hampshire</option>
<option value="NJ">New Jersey</option>
<option value="NM">New Mexico</option>
<option value="NY">New York</option>
<option value="NC">North Carolina</option>
<option value="ND">North Dakota</option>
<option value="OH">Ohio</option>
<option value="OK">Oklahoma</option>
<option value="OR">Oregon</option>
<option value="PA">Pennsylvania</option>
<option value="RI">Rhode Island</option>
<option value="SC">South Carolina</option>
<option value="SD">South Dakota</option>
<option value="TN">Tennessee</option>
<option value="TX">Texas</option>
<option value="UT">Utah</option>
<option value="VT">Vermont</option>
<option value="VA">Virginia</option>
<option value="WA">Washington</option>
<option value="WV">West Virginia</option>
<option value="WI">Wisconsin</option>
<option value="WY">Wyoming</option>
</select>
<label>State of City</label>
</div>
<div className="input-field col s2">
<button
className="btn waves-effect waves-light"
type="submit"
name="action"
>
Submit
<i className="material-icons right">cloud</i>
</button>
</div>
</Fragment>
);
};
export default UnitedStatesSelectionForm;
A couple takeaways:
- moment.js: This is very useful when dealing with time-related data in JavaScript. This made it possible to report accurate
sunrise
andsunset
times for a selected city instead of just using, say, your own local time which really wouldn't be accurate for another timezone (thetimezone
given from theopenweathermap
API is supplied as the shift in seconds from UTC). Basically, working with timezones is annoying in JavaScript. - Materialize: Don't forget to initialize!
- Conditional rendering: The linked to article gives a very nice overview of conditional rendering in React. He specifies the following ways even though I have just used the ternary way: if, if else, ternary, &&, switch case, multiple conditional renderings in React, nested conditional rendering in React, conditional rendering with higher-order components, and finally if else components in React. Clearly quite a few ways! He provides a nice summary to make it all more palatable:
- if
- most basic conditional rendering
- use to opt-out early from a rendering (guard pattern)
- cannot be used within return statement and JSX (except self invoking function)
- if-else
- use it rarely, because it's verbose
- instead, use ternary operator or logical && operator
- cannot be used inside return statement and JSX (except self invoking function)
- ternary operator
- use it instead of an if-else statement
- it can be used within JSX and return statement
- logical && operator
- use it when one side of the ternary operation would return null
- it can be used inside JSX and return statement
- switch case
- avoid using it, because it's too verbose
- instead, use enums
- cannot be used within JSX and return (except self invoking function)
- enums: multiple conditional renderings
- use it for conditional rendering based on multiple states
- perfect to map more than one condition
- nested conditional rendering
- avoid them for the sake of readability
- instead, split out components, use if statements, or use HOCs
- conditional rendering with higher-order components
- components can focus on their main purpose
- use HOC to shield away conditional rendering
- use multiple composable HOCs to shield away multiple conditional renderings
- external templating components: if else components
- avoid them and be comfortable with JSX and JS
- if
- Destructuring: Life will be so much easier if you can effectively destructure.
The last point above deserves another remark in the context of passing props. Depending on your application, you will often want to pass quite a few props. Everything can get out of control quickly if you aren't careful. Instead of passing all of the props individually inside of a component, you can spread the props. This makes it possible, for example, to specify what you want your component props to be and then to pass them as needed:
const headerProps = {temp, specificWeatherDesc, cityName, cityState, searchCount};
const modalProps = {genWeatherDesc, specificWeatherDesc, temp, feelsLike, low, high, cityName, iconURL, cityState, sunrise, sunset, searchCount, cityId, persistentTimeCount, sameCityState};
<Headers { ...headerProps } />
<Modal { ...modalProps } />
Then, inside of the Model
component, which is a class, we can do the following:
const {genWeatherDesc, specificWeatherDesc, temp, feelsLike, low, high, cityName, iconURL, cityState, sunrise, sunset, searchCount, cityId, persistentTimeCount, sameCityState} = this.props;
Since Headers
is a functional component, we can do the following:
const Headers = ({temp, specificWeatherDesc, cityName, cityState, searchCount}) => { }
The less you have to type the better!
Controlled forms: Managing forms with state
So far we have dealt with forms how you might normally deal with forms, namely doing something like the following in a class method (as we did with the city weather application in the above note):
searchCity = e => {
e.preventDefault();
const city = document.getElementById('city').value;
const cityState = document.getElementById('city-state').value;
this.getCityWeather(city, cityState);
}
Or consider something even more generic (i.e., the handleSubmit
method below):
import React, { Component } from 'react';
class FormPractice extends Component {
constructor(props) {
super(props);
this.state = {};
}
handleSubmit = e => {
e.preventDefault();
console.log('form submitted')
const name = document.getElementById('name').value;
console.log(name)
}
render() {
return (
<div className="container">
<div className="row">
<div className="col s6 offset-sm-3">
<form onSubmit={this.handleSubmit}>
<input type="text" placeholder="Enter a name" />
<input type="submit" value="submit" />
</form>
</div>
</div>
</div>
);
}
}
export default FormPractice;
What we have above is a value
property that's attached to every input
box (input text) that's kind of already managing state for us. When we submit the form, we can see that the DOM has already updated the <input type="text" placeholder="Enter a name" />
element's value
property to reflect what the value was when we submitted the form. The docs on forms even note this kind of behavior: "HTML form elements work a little bit differently from other DOM elements in React, because form elements naturally keep some internal state." They then give an example of a form in plain HTML that accepts a single name:
<form>
<label>
Name:
<input type="text" name="name" />
</label>
<input type="submit" value="Submit" />
</form>
They then note: "This form has the default HTML form behavior of browsing to a new page when the user submits the form. If you want this behavior in React, it just works. But in most cases, it's convenient to have a JavaScript function that handles the submission of the form and has access to the data that the user entered into the form. The standard way to achieve this is with a technique called controlled components."
What they're saying is we can always do something like document.getElementById('name').value
as you might normally do with JavaScript, but that is bad. That is not good in React. The main idea in React is that mutable state (anything changing inside of our application, especially data), including input
box values, is typically kept in the state property of components and only updated with setState
.
So you could do things "the vanilla JS way" where you grab the elements using something like document.getElementById(element).value
, but React wants state to be the single source of truth, meaning the value of the input box is going to be state, not the other way around. Thus, you will often see an input
element whose value
attributes reads from state and whose onChange
attribute is an event handler used to update state. Consider this example:
<label>Name (4 to 20 characters)
<input
id='input-name-example'
onChange={this.handleChange}
type='text'
name='chosenText'
value={this.state.chosenText}
minLength='4'
maxLength='20'
size='30'
/>
</label>
It is clear the value
attribute is the value of the chosenText
state property (if you don't initialize chosenText
, then React will give you a warning), but you have to update state (specifically chosenText
if you actually want the new value to be reflected in the input box). How do we do this? Understanding that the following two pieces of code are effectively the same will help:
showNameBad = e => {
const anExample = document.getElementById('input-name-example').value;
console.log(anExample);
}
showNameGood = e => {
console.log(e.target.value)
}
The e
referenced in both showName
methods above is the change (which is why onChange
makes sense in terms of naming what is effectively an event listener). So anytime the input-name-example
input box changes for any reason, then that is what's going to cause showName
to fire. The e
is the change--what is the target
of the change? It's the input
element itself, in this case the input
box with an ID of input-name-example
. And then, of course, we grab the value
from the input
box. To recap:
e
: The change itself.target
: The element on which there was a change.value
: The value of the target element on which there was a change.
Notice that the same thing is effectively happening above:
e
: The change itself.document.getElementById('input-name-example')
: The element whose value we suspected to be effected by the change--this element is theinput
box on which we want to manage changes.value
: The value of the target element on which there was a change (or not).
To make this example useful, we need to modify the showNameGood
method to update state to reflect the new value (otherwise the input box will never reflect this change and all the user will see is something that looks and feels like they aren't actually typing anything):
showNameGood = e => {
this.setState({
chosenText: e.target.value
});
}
So what's really happening here is that the user thinks they are changing the input box but they aren't really the ones changing it. What the user is doing is making some change on the input box that causes the onChange
event handler to fire, which itself often sets state to update with a new value, and when state was updated the render
method would run again, and finally React would change the input box.
In a typical JavaScript application, the DOM would manage this state internally for us, and we would simply pull values from the DOM as needed. But this is not the programming paradigm employed in React. Anything that changes is almost always going to go in state. Hence, for pretty much all form-related inputs, React wants to control them via state. The main exception is the input
with type='file'
. These tags are read-only: you can't write to a file field. So those are always going to be uncontrolled components. If you want the value of those, then you will need to get them the old-fashioned way.
The big win here is that we don't need to go and bother the DOM. We don't need to go and fetch an element or any of that. We've already got it saved in state. And it's React's job to make sure this process is efficient. See the next note for efficiently handling situations involving multiples inputs with only one onChange
event handler.
Handling multiple inputs with one onChange
event listener
When accepting multiple inputs from a user via a form, a common pattern starts to crop up in React: many of your onChange
event handlers will look very similar:
handleOpinion = e => {
this.setState({
userOpinion: e.target.value
});
}
handleFact = e => {
this.setState({
userFact: e.target.value
});
}
...
In the docs, React hints at a way to possibly deal with this potential problem (imagine having numerous event handlers like the above the event handler was basically doing the same thing every single time): The basic idea is to add a name
attribute to each element (where name
on the concerned element typically matches what you have named the state variable associated with the concerned element) and let the handler function choose what to do based on the value of event.target.name
.
For example, if you have a variable in state such as userText: 'Boilerplate'
, then an input tag where this value is meant to be effected might generically look like the following:
<input onChange={this.handleChange} type='text' name='userText' value={this.state.userText} />
And then our onChange
function would very often look like the following:
handleChange = event => {
let eventName = event.target.name;
let eventValue = event.target.value;
this.setState({
[eventName]: eventValue
});
}
The [eventName]
syntax simply lets us use a variable for an object key property in JavaScript (it's an ES6+ feature). It's worth nothing that the handleChange
handler function can get much more sophisticated and can even be useful for situations where name
on an element may not be what we are after so much as the type as is the case for an input
with type='checkbox'
(often you simply want to know whether or not a checkbox is checked or not, and this can be assessed by looking at event.target.checked
, not event.target.name
). Here's one minor example:
handleChange = (event) => {
const {target: {name: eventName, type: eventType, value: eventValue}} = event;
switch(eventType) {
case 'checkbox':
this.setState((prevState, props) => {
let {wantedFoods} = this.state;
let removalIndex = wantedFoods.indexOf(eventValue);
let newWantedFoods = wantedFoods.includes(eventValue)
? wantedFoods.slice(0, removalIndex).concat(wantedFoods.slice(removalIndex + 1))
: [...wantedFoods, eventValue];
return ({
[eventName]: !prevState[eventName],
wantedFoods: newWantedFoods
})
});
break;
case 'select-multiple':
this.setState({
chosenFruits: Array.from(event.target.selectedOptions, (item) => item.value)
});
break;
default:
this.setState({
[eventName]: eventValue
});
}
}
The overall point is that you can essentially have one robust handleChange
method (try not to overload it, but it's useful to know what your options are). Read this article on Medium for more info.
Data flows down so pass state up!
The need to so-called "lift state up" (or share state in different components despite not being children of each other) is rather common in React. Much of the need for lifting state up explicitly, as shown in the docs, will be mitigated when we start using Redux and/or the Context API.
"Lifting state up" means we are going from two or more components managing their own state locally to lifting up state to the most common ancestor of all the components that need access to the same piece(s) of state. The components will still have event handlers and the like, but those event handlers will now not be defined locally but passed down as props from the common ancestor so that the event handler changes state in the common ancestor as opposed to changing state in the local component.
In the example in the docs, both TemperatureInput
components will be passed an onChange
method (specifically named onTemperatureChange
even though it can be named anything that makes sense) to be used within TemperatureInput
as the onChange
event handler for an input
:
<!-- In Calculator.jsx -->
<TemperatureInput scale='f' temperature={fahrenheit} onTemperatureChange={this.handleTempChange} />
<!-- In TemperatureInput.jsx -->
handleChange = e => {
const {name: scale, value: temperature} = e.target;
this.props.onTemperatureChange(temperature, scale);
}
<input name={scale} onChange={this.handleChange} type='number' value={temperature} />
As can be seen above, within the TemperatureInput
component, we have an input
whose onChange
calls a method available as a prop, this.props.onTemperatureChange
, where the method made available as a prop changes the state of the common ancestor component instead of changing state locally (this.props.onTemperatureChange
changes the state of the Calculator
component and not the TemperatureInput
component).
In terms of architecture, when you realize two components are at different levels of the component tree (i.e., one is not the direct child of another) but both need access to the same piece of state, then the common practice is to lift the state to the first common ancestor and pass down through props whatever methods are necessary to change the state of the parent component--the method that actually changes the state of the parent component is called within one of the children components. Thus, essentially, a child component (e.g., TemperatureInput
) is changing the state of its parent component (e.g., Calculator
) by means of a method that lives in its parent component and changes state in its parent component but is called within itself (e.g., handleTempChange
lives in Calculator
and sets state in Calculator
but is called within TemperatureInput
because it is passed down as a prop). So when the code runs inside either input
box in TemperatureInput
, it's not going to manage its own state--it's going to be running its parent's function which is going to update its parent's state.
Here are the three files (somewhat refactored from what appears in the docs) with a subsequent recap of what happens throughout the process:
Calculator.jsx
import React, { Component, Fragment } from 'react';
import TemperatureInput from '../TemperatureInput/TemperatureInput'
import BoilingVerdict from '../BoilingVerdict/BoilingVerdict';
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
class Calculator extends Component {
constructor(props) {
super(props);
this.state = {
temperature: '',
scale: 'c'
}
}
handleTempChange = (temperature, scale) => {
switch(scale) {
case 'c':
this.setState({
scale: 'c',
temperature
});
break;
case 'f':
this.setState({
scale: 'f',
temperature
});
break;
default:
return;
}
}
render() {
const {scale, temperature} = this.state;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<Fragment>
<TemperatureInput
scale='c'
temperature={celsius}
onTemperatureChange={this.handleTempChange} />
<TemperatureInput
scale='f'
temperature={fahrenheit}
onTemperatureChange={this.handleTempChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</Fragment>
);
}
}
export default Calculator;
TemperatureInput.jsx
import React, { Component } from 'react';
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit',
};
class TemperatureInput extends Component {
handleChange = e => {
const {name: scale, value: temperature} = e.target;
this.props.onTemperatureChange(temperature, scale);
}
render() {
const { scale, temperature } = this.props;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input name={scale} onChange={this.handleChange} type='number' value={temperature} />
</fieldset>
);
}
}
export default TemperatureInput;
BoilingVerdict.jsx
import React from 'react';
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
export default BoilingVerdict;
Here's the recap for what happens when you edit an input:
1. React calls the function specified as onChange
on the DOM <input>
. In our case, this is the handleChange
method in the TemperatureInput
component:
handleChange = e => {
const {name: scale, value: temperature} = e.target;
this.props.onTemperatureChange(temperature, scale);
}
2. The handleChange
method in the TemperatureInput
component calls this.props.onTemperatureChange()
with the new desired value (i.e., temperature
) and scale. The props for TemperatureInput
, including onTemperatureChange
and scale
, were provided by its parent component, the Calculator
:
<TemperatureInput
scale='c'
temperature={celsius}
onTemperatureChange={this.handleTempChange} />
<TemperatureInput
scale='f'
temperature={fahrenheit}
onTemperatureChange={this.handleTempChange} />
3. The handleTempChange
method in Calculator
handleTempChange = (temperature, scale) => {
switch(scale) {
case 'c':
this.setState({
scale: 'c',
temperature
});
break;
case 'f':
this.setState({
scale: 'f',
temperature
});
break;
default:
return;
}
}
gets called regardless of which input
<input name={scale} onChange={this.handleChange} type='number' value={temperature} />
was edited within the TemperatureInput
component.
4. Inside the handleTempChange
method, as seen above, the Calculator
component asks React to re-render itself by calling this.setState()
with the new input value and the current scale of the input we just edited.
5. React calls the Calculator
component's render
method
render() {
const {scale, temperature} = this.state;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<Fragment>
<TemperatureInput
scale='c'
temperature={celsius}
onTemperatureChange={this.handleTempChange} />
<TemperatureInput
scale='f'
temperature={fahrenheit}
onTemperatureChange={this.handleTempChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</Fragment>
);
}
to learn what the UI should look like. The values of both inputs are recomputed based on the current temperature and the active scale. The temperature conversion is performed here.
6. React calls the render
methods of the individual TemperatureInput
components
render() {
const { scale, temperature } = this.props;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input name={scale} onChange={this.handleChange} type='number' value={temperature} />
</fieldset>
);
}
with their new props specified by the Calculator. It learns what their UI should look like.
7. React calls the render
method of the BoilingVerdict
component
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
passing the temperature in Celsius as its props.
8. React DOM updates the DOM with the boiling verdict and to match the desired input values. The input we just edited receives its current value, and the other input is updated to the temperature after conversion.
Styling components
If we look at the "style" subsection under "DOM Elements" in the API reference, we see that style
is often used in-line, but that should almost always be used for dynamic styling because in-line styles are often the least performant way to change styles (in-line styles also have a high degree of specificity in relation to other stylesheets and the like). Additionally, as will be seen below from the docs, in-line styles lend themselves well to conditional styling (this falls under "dynamic styling" but should specifically be noted because of how useful it can be). That said, much of what is said in the docs about styling is reproduced below:
Some examples in the documentation use style
for convenience, but using the style
attribute as the primary means of styling elements is generally not recommended. In most cases, className
should be used to reference classes defined in an external CSS stylesheet. style
is most often used in React applications to add dynamically-computed styles at render time. See also FAQ: Styling and CSS.
The style
attribute accepts a JavaScript object with camelCased properties rather than a CSS string. This is consistent with the DOM style
JavaScript property, is more efficient, and prevents XSS security holes. For example:
const divStyle = {
color: 'blue',
backgroundImage: 'url(' + imgUrl + ')',
};
function HelloWorldComponent() {
return <div style={divStyle}>Hello World!</div>;
}
Note that styles are not autoprefixed. To support older browsers, you need to supply corresponding style properties:
const divStyle = {
WebkitTransition: 'all', // note the capital 'W' here
msTransition: 'all' // 'ms' is the only lowercase vendor prefix
};
function ComponentWithTransition() {
return <div style={divStyle}>This should work cross-browser</div>;
}
Style keys are camelCased in order to be consistent with accessing the properties on DOM nodes from JS (e.g. node.style.backgroundImage
). Vendor prefixes other than ms should begin with a capital letter. This is why WebkitTransition
has an uppercase “W”.
React will automatically append a “px” suffix to certain numeric inline style properties. If you want to use units other than “px”, specify the value as a string with the desired unit. For example:
// Result style: '10px'
<div style={{ height: 10 }}>
Hello World!
</div>
// Result style: '10%'
<div style={{ height: '10%' }}>
Hello World!
</div>
Not all style properties are converted to pixel strings though. Certain ones remain unitless (e.g., zoom
, order
, flex
). A complete list of unitless properties can be seen here.
React Router
Introduction to React Router and why you may want to use it
Before jumping into anything concerning React Router, here are some useful links for reference:
- Official Documentation
- Quick Start Docs for React Router on the Web
- Philosophy of React Router
- GitHub Home for react-router
- NPM Home for react-router
- GitHub Home for react-router-dom
- NPM Home for react-router-dom
And here is the message we are greeted with at the homepage for React Router: "Components are the heart of React's powerful, declarative programming model. React Router is a collection of navigational components that compose declaratively with your application. Whether you want to have bookmarkable URLs for your web app or a composable way to navigate in React Native, React Router works wherever React is rendering--so take your pick!" Web | Native
Now we get to the important question: Why would you want to use React Router? Well, React Router does a couple things. First of all, as the docs note, it's just a collection of navigational components. So that's how we are going to use React Router. We are going to install it with NPM and they are going to give us a handful of components that we can use and not make for ourselves (e.g., <Route>
, <Link>
, etc.), and they allow us to control the URL bar. That's really really big because it does two things:
- It gives us bookmarkable URLs (or "deep links"). Why does this matter? Well, suppose you wanted to share a story, article, or something else on the web with a friend, but whatever link you gave always rendered the homepage (as would happen right now with the AWS flashcards app done previously). That is obviously not desired behavior. If you send someone a link, then you want the link to point to the resource you are trying to share. Essentially, we need to make sure the user can get back to a given page using the URL.
- It makes it possible for us to use the forward and backward arrows in the browser. Right now (e.g., in the AWS flashcards app) if we were to use the forward or backward arrows in the browser, then everything would be lost. Of course, that is not desirable behavior either!
So React Router's job is to give us the ability to make a "real" single-page application where we are manipulating the DOM we have been with React, but we can use the URL bar. This usefulness of this will become more and more apparent, especially as you manage applications of any size.
React Router docs reference (for web applications)
Here are the docs links for React Router (for a web application):
- Announcements
- Examples
- Guides
- API
Getting started: Router
, Route
, and Link
The first basic example in the docs has the following import
statement:
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
We will get to Switch
in due time (a <Switch>
looks through its children <Route>s
and renders the first one that matches the current URL), but we will focus on Router
, Route
, and Link
for right now.
Note: Before moving on, make sure to note that most of what you will import from react-router-dom
will be components. And the docs spell out more about these components take as props, but everything we have learned about components previously apply just as well to components coming from react-router-dom
, only we get extra goodies, more sophisticated behavior, etc., due to code someone else wrote (this is just how it is with components you might import from other libraries as well).
1. BrowserRouter: The first thing to take note of is that the Router
(we import BrowserRouter as Router
mostly to keep with how things are done with React Native--it makes the transition from one thing to the next much simpler and smoother) goes around everything that it needs to control. So generally speaking that's going to be at the application level (i.e., you will often find yourself wrapping everything in the App
component in Router
):
function App() {
return (
<Router>
<div className="App">
...
</div>
</Router>
);
}
Note that Router
will be the single DOM element that we are actually going to be exporting. Now, when we get to Redux, or there might be other situations where something is around the Router
, but every component that the Router
needs to be able to manage needs to be contained within Router
. And that's important because we only ever want one instance of the Router
. We don't ever want more than one Router
component unless you really know what you're doing--we want to keep it down to just one because we only have one URL bar to work with.
2. Route and Switch: The second thing we need now is an actual route! So this is what we use to determine what happens in our application when we are at a given URL (this will make more sense momentarily). For example:
function App() {
return (
<Router>
<Route path='/' component={Home} />
</Router>
);
}
As with most things, the docs give us the most insight as to what is going on underneath the hood. The authors even put in a good note about the importance of understanding the Route
component and learning how to use it well: "The Route component is perhaps the most important component in React Router to understand and learn to use well. Its most basic responsibility is to render some UI when its path matches the current URL."
In terms of what is shown above, the path='/'
prop means React Router is going to look to see if the URL has the specified path in it (namely '/'
in this case), and if it does, then it's going to render the Home
component.
Note: As noted in the docs concerning Route
render methods, the authors suggest using children
elements over component
or render
(especially with the addition of Hooks). They note we should only ever use one of the component
, render
, or children
props on a given Route
.
In any case, consider the following chunk of code that may give somewhat unexpected results at first:
import React from 'react';
import './App.css';
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom';
import Home from './components/Home/Home.component';
import About from './components/About/About.component';
function App() {
return (
<Router>
<Route path='/' component={Home} />
<Route path='/about' component={About} />
</Router>
);
}
export default App;
If we visited http://localhost:3000
(or whatever port your application is listening on), then we would just get the Home
component. Makes sense. If we http://localhost:3000/about
, however, we would actually get both the Home
component as well as the About
component. Why? This has to do with how path is treated, specifically in terms of how it uses the path-to-regexp library which utilizes partial matching by default. Essentially, both paths are matched because both paths contain /
. This is clearly undesired behavior, and there are a few ways of going about addressing it (next we discuss use of the exact prop which needs to be used with some care, especially if you have nested routes at some point--we will remark on this soon). Might this possible be a chance to use a Switch
component (recall that a <Switch>
looks through its children <Route>s
and renders the first one that matches the current URL)? Would something like
function App() {
return (
<Router>
<Switch>
<Route exact={true} path='/' component={Home} />
<Route path='/about' component={About} />
</Switch>
</Router>
);
}
only render the About
component if we visited http://localhost:3000/about
? No, only the Home
component would render! Why? Because the Home
component was matched first, and the Switch
component effectively results in only considering the first Route
with a path
whose URL matches the current one. Since both /
and /about
match /about
, but path='/'
comes first above, we end up simply getting the Home
component. Of course, we could just switch the order of the Route
s and our problem would be solved.
As mentioned above, we have to be somewhat careful with using the exact
prop, especially if our application starts to grow and we want to have Route
s inside of multiple components. Then you could be looking at a bunch of nested routes, and using the exact
prop could unintentionally prevent matching for the nested routes. This answer on Stack Overflow does a great job of explaining the utility of using the Switch
component along with not using exact
:
Suppose the Home route contains nested Route
s like the following:
const Home = (props) => {
return (
<Fragment>
<p>This will always appear in Home :)</p>
<Route path='/dashboard' component={Dashboard}></Route>
<Route path='/layout' component={Layout}></Route>
</Fragment>
);
}
So now if you write
<Route exact path="/" component={Home} />
then when you visit /dashboard
, the Dashboard
component you want to render cannot be rendered since no Route matches with /dashboard
at the top level. In order to achieve the desired behavior, you can make use of Switch
and reorder the routes so that the paths that are prefixes to other paths are at the end:
<Switch>
<Route path='/about' component={About} />
<Route path='/' component={Home} />
</Switch>
The effect of the example code above from the SO answer is that we can visit http://localhost:3000
and only have the Home
component rendered (i.e., without any of its nested Route
s, specifically Dashboard
and Layout
, being rendered). We can visit http://localhost:3000/about
and have only the About
component rendered (even though /
is a partial match, since we are inside of Switch
, only the About
component will be rendered), as expected. But the great thing happens when we visit http://localhost:3000/dashboard
or http://localhost:3000/layout
: Since /
is a partial match for /dashboard
and/or /layout
, we get the Home
component and also whatever routes inside match the current URL, namely /dashboard
or /layout
. The point is that you can use Switch
along with exact
in powerful ways--simply don't get carried away with exact
lest you prevent yourself from useful nested routing.
The awesome thing about all of this is that we are basically getting conditional rendering based on what is in the URL bar, and that is totally awesome!
3. Link: For a Link, we do not use anchor tags! Why? Because that will take us away from our application. Think about it like this: When we load up our application for the first time, what loads up is index.html
. We are using a front-end framework, and if we leave index.html
, then everything has to start back over again. It's not like a back-end framework. As the docs note, behind the scenes, <Link>
renders an <a>
with a real href
, so people using the keyboard for navigation or screen readers will still be able to use the app. Whatever the case, under Link
in the docs, the authors make it clear that the Link
component is intended to provide declarative, accessible navigation around our application (not outside of it!). So how do we use Link
?
The most common prop you will use with Link
is the to
prop (that actually comes in three forms, namely to: string
, to: object
, to: function
, all of which have varying powers but the most basic and common one is simply to: string
). And we say where we want to go (among other things sometimes):
function App() {
return (
<Router>
<h1>Header!</h1>
<div>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
</div>
<Route exact path='/' component={Home} />
<Route path='/about' component={About} />
<h1>Footer!</h1>
</Router>
);
}
If we click on the Link
with "Home", then React Router will send us along to /
, where the Home
component is being rendered. React Router is going to keep an eye on the URL, and when it sees that we are now at /
, then it will load the Home
component. The same thing goes if we click the Link
with "About". React Router will send us along to /about
, whereby React Router will then see that we are supposed to render the About
component.
The cool thing with using Link
s like this is that now we can use the forward and backward arrows in our browser:
As can be seen above, we have actual anchor tags as if we had made them (even though React Router made them for us), but the difference is that as we hop back and forth within the application, we can see the purple blinking which means the DOM is changing, and now we can use the forward and backward arrows without any issue because React Router is doing two things:
- It's deciding which component to render.
- We're not leaving
index.html
. We're not leaving our application. It's not reloading. JavaScript is simply rewriting what's in the URL, and so it makes it look like we are reloading our application on different pages, but really we simply conditionally rendering all sorts of different components based on routes that we set up.
Recap (on the basics of how React Router works):
- The
Router
itself is going to be used only one time and it will wrap around everything in our application. - The
Route
can be used anytime we want to render a component(s) based on whatever URL the user happens to be at. - The
Link
is going to be our new substitute for anchor tags--if we want to link internally inside of our application, then we are always going to useLink
.
NavLink
and making a NavBar
component
One thing that is rather cool with React is all of the pre-built components people have put together for ease of use. For example, Materialize has an entire sidebar of components (Badges, Buttons, Breadcrumbs, etc.) we can pick and choose from. The one we are interested in right now is the Navbar component. For a basic navbar with right-aligned links and a left-aligned logo, we get the following code:
<nav>
<div class="nav-wrapper">
<a href="#" class="brand-logo">Logo</a>
<ul id="nav-mobile" class="right hide-on-med-and-down">
<li><a href="sass.html">Sass</a></li>
<li><a href="badges.html">Components</a></li>
<li><a href="collapsible.html">JavaScript</a></li>
</ul>
</div>
</nav>
As is the case when we bring in anything from the outside, we need to be mindful of the particulars of whatever framework we are using if, in fact, we are using one. For example, if we are using React and React Router, as we are right now, then we need to make some modifications to the code above, namely change all of the a
tags to Link
tags and all of the href
s to to
s and specify where the Link
s should route to (assuming we want to keep everything within our application).
To use the resource above, we can get the Materialize CDN and drop it in our index.html
underneath our title
:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
And then we can make a sample NavBar
component using the code above:
import React from 'react';
import {Link} from 'react-router-dom';
const NavBar = (props) => {
return (
<nav className="black">
<div className="nav-wrapper">
<Link to="/" className="brand-logo">AirBnB</Link>
<ul id="nav-mobile" className="right hide-on-med-and-down">
<li><Link to="/host">Become a host</Link></li>
<li><Link to="/help">Help</Link></li>
<li><Link to="/login">Log in</Link></li>
<li><Link to="/signup">Sign up</Link></li>
</ul>
</div>
</nav>
);
}
export default NavBar;
Then our App.js
file could look something like the following for a very basic use case (of course, in a real application we would put the components in separate files):
import React from 'react';
import './App.css';
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom';
import NavBar from './components/NavBar/NavBar.component';
// For demo purposes with router using materialize
const Home = () => <h1>Home</h1>
const Host = () => <h1>Host</h1>
const Help = () => <h1>Help</h1>
const LogIn = () => <h1>Log in</h1>
const SignUp = () => <h1>Sign up</h1>
function App() {
return (
<Router>
<NavBar />
<Route exact path='/' component={Home} />
<Route exact path='/host' component={Host} />
<Route exact path='/help' component={Help} />
<Route exact path='/login' component={LogIn} />
<Route exact path='/signup' component={SignUp} />
</Router>
);
}
export default App;
To make all of this even cooler, there is a NavLink component from React Router that we may use, where NavLink
is, "A special version of the <Link>
that will add styling attributes to the rendered element when it matches the current URL." They then give several props that make the NavLink
special and easy to use:
activeClassName: string
activeStyle: object
exact: bool
strict: bool
isActive: func
location: object
aria-current: string
As the authors note for activeClassName
, this is the class to give the element when it is active. The default given class is active
. This will be joined with the className
prop. So you can use the default active
and style as desired or you can set activeClassName
manually which may not be such a bad idea (e.g., activeNavLink
).
In our situation, we should turn all of the Link
s in our NavBar
component to NavLink
s:
import React from 'react';
import {NavLink} from 'react-router-dom';
const NavBar = (props) => {
return (
<nav className="black">
<div className="nav-wrapper">
<NavLink to="/" className="brand-logo">AirBnB</NavLink>
<ul id="nav-mobile" className="right hide-on-med-and-down">
<li><NavLink to="/host">Become a host</NavLink></li>
<li><NavLink to="/help">Help</NavLink></li>
<li><NavLink to="/login">Log in</NavLink></li>
<li><NavLink to="/signup">Sign up</NavLink></li>
</ul>
</div>
</nav>
);
}
export default NavBar;
And now we can watch this in action:
Note that the partial matching is in effect, namely, since we are at http://localhost:3000
, the NavLink
with className='brand-logo'
constantly has active
as also part of its joined className
since that route is active. Also note how subsequent routes that are visited automatically have a className
of active
added to them. As noted previously, active
is the default className
added when a NavLink
is active, but we can use the activeClassName
prop to customize this if we so wish
The upshot of all of this is that we can selectively style different navigation links that are active:
nav .active {
text-decoration: underline;
}
The NavLink
largely gives you a lot of styling power.
Route
s with the component
prop vs the render
prop
We are now going to look at another very important prop of the Route
component, namely render
(so far we have just been looking at component
). It should be noted at the outset that in the Route render methods section of the docs that the recommended method of rendering something with a <Route>
is to use children
elements (so neither component
nor render
). Regardless, only one of component
, render
, or children
should be used on a given <Route>
. For the children
prop, the docs note the following: "Sometimes you need to render whether the path matches the location or not. In these cases, you can use the function children
prop. It works exactly like render
except that it gets called whether there is a match or not. The children
render prop receives all the same route props (i.e., the three route props match
, location
, and history
accompany all of the render methods) as the component
and render
methods, except when a route fails to match the URL, then match
is null
. This allows you to dynamically adjust your UI based on whether or not the route matches."
Returning to our specific situation, what we have done so far is simply look for a route match and then render a single component. Basically, a <Route>
is just a fancy if
statement: If the path
is matched, then render the specified component:
<Route exact path='/' component={Home} />
This poses a problem. Why? Because what if we wanted to do something like the following:
<Route exact path='/' component={<Home title='Hello'>} />
This is what we have normally done if we want to send down props and the like. But this is not okay to pass the component
prop. Let's take a look at what the docs say about each render method: component, render, and children.
- component: A React component to render only when the location matches. It will be rendered with route props. When you use
component
(instead ofrender
orchildren
) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to thecomponent
prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use therender
or thechildren
prop. - render (func): This allows for convenient inline rendering and wrapping without the undesired remounting explained above. Instead of having a new React element created for you using the
component
prop, you can pass in a function to be called when the location matches. Therender
prop function has access to all the same route props (match
,location
andhistory
) as thecomponent
render prop. Note:<Route component>
takes precedence over<Route render>
so don't use both in the same<Route>
. - children (func): Sometimes you need to render whether the path matches the location or not. In these cases, you can use the function
children
prop. It works exactly likerender
except that it gets called whether there is a match or not. Thechildren
render prop receives all the same route props as thecomponent
andrender
methods, except when a route fails to match the URL, thenmatch
isnull
. This allows you to dynamically adjust your UI based on whether or not the route matches. This could also be useful for animations. Note:<Route children>
takes precedence over both<Route component>
and<Route render>
so don't use more than one in the same<Route>
.
In terms of render
, we can do something like the following now instead:
<Route exact path='/' render={() => (<Home title='Hello!' />)} />
So render
is very very important because anytime you need to do more than just render a component (e.g., pass certain props and the like), then you'll use render
(or children
) instead of component
. But for anytime when all you need to do is render a component and don't need to pass anything down, then component
is the right render method to use.
Nested routes
We are now going to explore the idea of nested routing.
Let's say we had multiple things inside of the Help
component that needed to happen. For instance, maybe we have on the Help
component a bunch of stuff that loads up at the top. So maybe we always want a header, an image, etc:
import React, { Fragment } from 'react';
const Help = (props) => {
return (
<Fragment>
<h1>Help Header</h1>
<p>An image goes here</p>
<h3>Footer for help</h3>
</Fragment>
);
};
export default Help;
So right now the Help
component would show everything above if we went to /help
. But maybe there's dynamic content that we need to account for. This is where Route
can be handy. Our Help
component could look like the following:
import React, { Fragment } from 'react';
import {Route, Link} from 'react-router-dom';
const HelpCustomer = () => <h1>Help Customer!</h1>
const HelpHost = () => <h1>Help Host!</h1>
const Help = (props) => {
return (
<Fragment>
<div>
<Link to='/help/customer'>I am a customer</Link> |
<Link to='/help/host'>I am a host</Link>
</div>
<p>An image goes here</p>
<Route path='/help/customer' component={HelpCustomer} />
<Route path='/help/host' component={HelpHost} />
<h3>Footer for help</h3>
</Fragment>
);
};
export default Help;
Back in our App.js
, however, this will not do (for reasons previously addressed in the note concerning Switch
):
function App() {
return (
<Router>
<NavBar />
<Route exact path='/' render={() => (<Home title='Hello!' />)} />
<Route exact path='/host' component={Host} />
<Route exact path='/help' component={Help} />
<Route exact path='/login' component={LogIn} />
<Route exact path='/signup' component={SignUp} />
</Router>
);
}
What's the problem here? Can you see why the Route
s with path
props /help/customer
and /help/host
would never be matched? This is because only exact
ly /help
is currently being matched within App.js
. The solution is to remove exact
. Again, as noted previously, be careful with your use of exact
. It can cause problems when used carelessly, especially in the context of using nested routing.
The docs on Route
component props: match
, location
, and history
As mentioned in the docs, all three render methods on <Route>
(i.e., component
, render
, and children
) will be passed the same three route props:
Let's consider what the docs have to say about each of these props before exploring them in more detail in the context of our current dummy application (i.e., the AirBnB navbar clone).
match
A match
object contains information about how a <Route path>
matched the URL. match
objects contain the following properties:
params
(object): Key/value pairs parsed from the URL corresponding to the dynamic segments of the pathisExact
(boolean):true
if the entire URL was matched (no trailing characters)path
(string): The path pattern used to match. Useful for building nested<Route>
surl
(string): The matched portion of the URL. Useful for building nested<Link>
s
You'll have access to match objects in various places:
- Route component as
this.props.match
- Route render as
({ match }) => ()
- Route children as
({ match }) => ()
- withRouter as
this.props.match
- matchPath as the return value
If a Route does not have a path
, and therefore always matches, you'll get the closest parent match. Same goes for withRouter
.
null matches
A <Route>
that uses the children
prop will call its children
function even when the route's path
does not match the current location. When this is the case, the match
will be null
. Being able to render a <Route>
's contents when it does match can be useful, but certain challenges arise from this situation.
The default way to "resolve" URLs is to join the match.url
string to the "relative" path.
If you attempt to do this when the match is null
, you will end up with a TypeError
. This means that it is considered unsafe to attempt to join "relative" paths inside of a <Route>
when using the children
prop.
A similar, but more subtle situation occurs when you use a pathless <Route>
inside of a <Route>
that generates a null
match object.
Pathless <Route>
s inherit their match
object from their parent. If their parent match
is null
, then their match
will also be null
. This means that
- a) any child routes/links will have to be absolute because there is no parent to resolve with and
- b) a pathless route whose parent
match
can benull
will need to use thechildren
prop to render.
location
Locations represent where the app is now, where you want it to go, or even where it was. It looks something like this:
{
key: 'ac3df4', // not with HashHistory!
pathname: '/somewhere',
search: '?some=search-string',
hash: '#howdy',
state: {
[userDefined]: true
}
}
The router will provide you with a location object in a few places:
- Route component as
this.props.location
- Route render as
({ location }) => ()
- Route children as
({ location }) => ()
- withRouter as
this.props.location
It is also found on history.location
but you shouldn't use that because it's mutable. You can read more about that in the history doc.
A location object is never mutated so you can use it in the lifecycle hooks to determine when navigation happens, this is really useful for data fetching and animation.
You can provide locations instead of strings to the various places that navigate:
Normally you just use a string, but if you need to add some "location state" that will be available whenever the app returns to that specific location, you can use a location object instead. This is useful if you want to branch UI based on navigation history instead of just paths (like modals).
Finally, you can pass a location to the following components:
This will prevent them from using the actual location in the router's state. This is useful for animation and pending navigation, or any time you want to trick a component into rendering at a different location than the real one.
history
The term "history" and "history
object" in this documentation refers to the history package, which is one of only 2 major dependencies of React Router (besides React itself), and which provides several different implementations for managing session history in JavaScript in various environments.
The following terms are also used:
- "browser history" - A DOM-specific implementation, useful in web browsers that support the HTML5 history API
- "hash history" - A DOM-specific implementation for legacy web browsers
- "memory history" - An in-memory history implementation, useful in testing and non-DOM environments like React Native
history
objects typically have the following properties and methods:
length
(number): The number of entries in the history stackaction
(string): The current action (PUSH
,REPLACE
, orPOP
)location
(object): The current location. May have the following properties:pathname
(string): The path of the URLsearch
(string): The URL query stringhash
(string): The URL hash fragmentstate
(object): location-specific state that was provided to e.g.push(path, state)
when this location was pushed onto the stack. Only available in browser and memory history.
push(path, [state])
(function): Pushes a new entry onto the history stackreplace(path, [state])
(function): Replaces the current entry on the history stackgo(n)
(function): Moves the pointer in the history stack by n entriesgoBack()
(function): Equivalent togo(-1)
goForward()
(function): Equivalent togo(1)
block(prompt)
(function): Prevents navigation (see the history docs)
history is mutable
The history
object is mutable. Therefore it is recommended to access the location from the render props of <Route>
, not from history.location
. This ensures your assumptions about React are correct in lifecycle hooks. For example:
class Comp extends React.Component {
componentDidUpdate(prevProps) {
// will be true
const locationChanged =
this.props.location !== prevProps.location;
// INCORRECT, will *always* be false because history is mutable.
const locationChanged =
this.props.history.location !== prevProps.history.location;
}
}
<Route component={Comp} />;
This is because a new location
object is generated for every <Route>
render whereas history.location
changes based on current location (i.e., history.location
is never generated anew but simply mutates to reflect whereever we are currently). In the example above, this.props.location !== prevProps.location
gives the desired result because the location
object for the current prop is different from the location
object for the previous prop. But this.props.history.location !== prevProps.history.location
gives a misleading conclusion because history.location
is always the same because it mutates to reflect current location; that is, different location
objects are being compared in the this.props.location !== prevProps.location
comparison whereas history.location
is the same object in the this.props.history.location !== prevProps.history.location
comparison.
Additional properties may also be present depending on the implementation you're using. Please refer to the history documentation for more details.
Using Route
component props in our application
We are now going to take a look at the <Route>
props that React Routers gives us (remarked on more extensively from the docs in the note above): match
, location
, and history
. These props release a lot of the programmatic power we have over the router.
The first thing to take note of is that a component will only have access to these props if the components is rendered from a <Route>
. So how can we get something like the NavBar
component to render from a <Route>
on every single page? As the docs note, <Route>
s without a path always match. So we can simply do something like the following for the time being:
<Route component={NavBar} />
If we do this, then we can console.log(props)
from within NavBar
, and when the NavBar
component is rendered, we will have access to all of the render props mentioned above. For example, if we have http://localhost:3000/help?name=frank&occupation=codeninja#awesome
in the URL bar and enter this, then we will get something like the following in the console:
This is sort of a shortened version of what you might get in native JavaScript with window.location
. From the image above, we can see a lot of the functions or methods that can be run against the history
object (see the note above or the docs for more on these methods). 9
It's worth noting that the history
object is a stack; that is, it is LIFO (last in first out) as opposed to a queue which is FIFO (first in first out). The length
property on the history
object shows how many things are on the stack.
One potential "gotcha" may occur when trying to access match
, location
, or history
from a component rendered by <Route>
. Specifically, the "gotcha" arises from what render method we are using (i.e., component
, render
, or children
). The following rules generally apply (see the docs) for accessing these props from the given render method:
component
:this.props.match|location|history
render
:({ match|location|history }) => ()
children
:({ match|location|history }) => ()
withRouter
:this.props.match|location|history
If you are using either render
or children
, then one easy way of getting all of the <Route>
props easily is to do something like the following:
<Route exact path='/' render={({...props}) => (<Home title='Hello!' {...props} />)} />
This takes advantage of ES6+ syntax and thus if we console.log(props)
from within the Home
component we will now get something like the following:
The push
object on history
can be useful when wanting to send the user somewhere for some reason besides just a click event. Because the history
object is a stack, if we push something on to the top of it, then it will make that the first thing to come off of it. So if we wanted to programmatically move someone around (i.e., so far we have been moving someone around only because they have clicked on a link or something like that. but there are times when you are going to want to move the user when they did not click on something because something happens such as another event, an AJAX request fulfills, etc.), then push
lends itself well to this.
As sort of a dummy example, consider modifying the Home
component in the following way:
import React, {Fragment} from 'react';
import {Route} from 'react-router-dom';
const Home = (props) => {
const {title} = props;
setTimeout(() => {
props.history.push('/help')
}, 2000)
return (
<Fragment>
<h1>Home! {title}</h1>
</Fragment>
);
}
export default Home;
Of course, this is not something we would want to leave on for very long because our home page is now useless, but the browser will wait two seconds (per the setTimeout
), and then it will call the push
method against our history
object, and it will push the browser forward to the /help
route or, rather, set the /help
route at the top of the stack.
So that's how the stack works: we end up at whatever is at the top of the stack. Going backwards with goBack
works the same way:
setTimeout(() => {
props.history.goBack()
}, 2000)
This would take us to the previous thing in the stack. And goForward
does just the opposite of goBack
(so long as there is actually something to goForward
to).
It's worth noting that there is a block method that may be useful sometimes in the sense that it gives the user the chance to confirm or deny something before leaving a page and/or other stuff firing off.
Using the match
prop on Route
for dynamic URLs and URL params
What is going to be remarked on now is basically how req.params
works in Express, namely the match.params
property will be an object containing properties mapped to the named route parameters. For example, if we had something like
<Route exact path='/about/:personId/name/:personName' component={About} />
and then visitied http://localhost:3000/about/7/name/william-wallace
, then match.params
would look as follows:
{
personId: '7',
personName: 'william-wallace'
}
In any case, this can be very valuable for managing a lot of different content that should really be grouped.
Redux
Redux docs reference
Here is the homepage for Redux and here is the GitHub repo. And here are docs links for reference:
- Introduction
- Basic Tutorial
- Advanced Tutorial
- Recipes
- FAQ
- Style Guide
- Other
- API Reference
- Redux Toolkit
React Redux docs reference
Here is the homepage for React Redux and here is the GitHub repo. And here are docs links for reference:
- Introduction
- Using React Redux
- API Reference
- Guides
Before Redux and React-Redux
If we hop on over to the Redux homepage, then we are greeted by the following four prominent messages after the centerpiece message (i.e., Redux - A Predictable State Container for JS Apps):
- Predictable: Redux helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test.
- Centralized: Centralizing your application's state and logic enables powerful capabilities like undo/redo, state persistence, and much more.
- Debuggable: The Redux DevTools make it easy to trace when, where, why, and how your application's state changed. Redux's architecture lets you log changes, use "time-travel debugging", and even send complete error reports to a server.
- Flexible: Redux works with any UI layer, and has a large ecosystem of addons to fit your needs.
We then see two other things underneath these messages (other libraries from the Redux team):
- React-Redux: Official React bindings for Redux
- Redux Toolkit: The official, opinionated, batteries-included toolset for efficient Redux development.
If we go to the React-Redux page (which is where we will spend most of our time), then we are greeted by four more prominent messages:
- Official: React Redux is maintained by the Redux team, and kept up-to-date with the latest APIs from Redux and React.
- Predictable: Designed to work with React's component model. You define how to extract the values your component needs from Redux, and your component receives them as props.
- Encapsulated: Creates wrapper components that manage the store interaction logic for you, so you don't have to write it yourself.
- Optimized: Automatically implements complex performance optimizations, so that your own component only re-renders when the data it needs has actually changed.
What is Redux?
In the simplest of terms, Redux is a state management tool/library (i.e., a bunch of JavaScript someone else wrote to make your life easier). When used properly, Redux will make your life much easier. If used properly. But starting out can be rough. The amount of boilerplate code to get things started can be rather intimidating. In fact, now there is an official "Redux+JS" template for Create React App to be used like so:
npx create-react-app my-app --template redux
There isn't much said about usage of this in the docs so it's hard to envisage exactly what the template provides. This video goes through some of it.
Returning to our original query, what could a state management tool or library really provide for us? How would using something like Redux differ from what we have done previously? Well, what have we done previously? Thus far, we have done entirely component-based state management. That is, state has always been managed either locally within a single component (e.g., making an AJAX call within componentDidMount
and then placing the results in local state) or we "lifted state up" so that a child component could pretend to change state in its parent component by using a callback passed down as props from the parent that actually changed the state in the parent.
Recall why "lifting up state" was necessary in the AWS flashcards application: An action happened in the quizbar component and that event needed to be known by the flashcard component, but these components were not children of each other but lived on other ends of the component hierarchy. We got around this problem by placing the state we needed in both components in the closest common ancestor, which happened to be the App
component. Nonetheless, we are still only managing state at the component level. This isn't application state at this point because even though it may seem like the QuizBar
component is changing the App
state, it's not. App
manages its own state, and App
sends down a callback as props to let QuizBar
know, "Hey, if something happens, run this code. I will change my own state. You don't ever change my state. I change my own state. You just run a little bit of code (the callback I pass down as props), and that will be me updating state. But don't worry about the details. That's not your problem." It's a fantastic solution. But only when the application is small.
When you get to a company that uses Redux for a very large application, you will have hundreds if not thousands of components working in unison, and it will be common for different components to know about a common piece of state (i.e., when something happens in one component that changes state, then another component somewhere else will need to know how state just changed):
In the picture above, you can imagine the pain and torment that would be involved in lifting up state so much. In general, as the application grows and gets much bigger, eventually everything is going to end up living at the very top in App
. And then App
is going to get super bloated and be an absolute disaster because you're going to end up sending down callbacks ten layers deep and you're going to be changing state in components where you have no idea what it's doing, and there's no way another developer is going to be able to sit down and understand what's happening. This is where Redux is going to come into play.
Redux is going to create something called the Store. We will have a Store that is sort of external to our application. Or it's outside of the component structure. And it will create a publication/subscription type model where, say, something happens in one component and then the component where something just happened can push or publish out to the store communicating what just happened. Meanwhile, a component elsewhere that needs to know about what just happened in the aforementioned component can subscribe to the store and be notified when that thing happens:
This is a super simple overview of how Redux works, but the idea is that Redux is going to work as a sort of global store where we can keep application state. Not component state. Because we are still going to use component state as we have, but we are going to start using Redux which will give us the ability to put stuff in a place where any component can use it. The upshot of all this is we can add components in various locations without having to worry about lifting up state to ridiculous levels:
One last thought: Redux is primarily meant for large applications. There is no way we are going to be able to make an applications of this size that justifies Redux right now, but we can implement it in a basic fashion to show how it might be used in a larger application. Regardless, it is absolutely phenomenal for managing application state in an easy and maintainable way that can be scaled over a long period of time across a whole bunch of developers because it creates good uniformity for how the whole application manages its state.
Redux and React
Probably the worst thing you can do with Redux is to just start coding. There are a lot of moving pieces. One of the questions we want to answer is how to work with Redux properly, and the problem is that the how really needs to be done the right way (as opposed to, "eh, it sort of works") in order to manage the entire application state effectively. So it will feel confusing and somewhat contrived at first possibly. It never gets more complicated than at the very beginning. So you just have to get to that point where you understand all of the moving pieces, why they are the way they are, and then you'll simply redo the same thing over and over and over. So what are the pieces!? We will cover them in two stages:
- React Redux: We will talk about how Redux works with React through the react-redux package just to get a sense of how the puzzle pieces might fit together.
- Redux: We will talk about how Redux itself works (we do not have to be using React to use Redux).
We will now cover the first piece. In any application using Redux and React, we have a couple of distinct pieces, namely the Redux piece as well as the React piece. Redux has no idea that React exists. It's not going to and it doesn't need to know. Meanwhile, React has no idea that Redux exists. In the same way, it doesn't need to know, and we are going to keep them completely separate. Redux is not meant to work exclusively with React. You could use Redux with Vue, Angular, or simply by itself (just use the JavaScript), etc. React obviously doesn't need Redux because we haven't been using Redux up until now. So Redux and React are totally distinct from each other.
The point of all of this is that some of the things we are going to talk about are specific to Redux while some things are specific to React. For example, with Redux, there are actions, reducers, and dispatchers, and all of these things have nothing to do with React, and React will not know that any of them exist. Of course, React has its own state, components, and everything else that makes React what it is, and Redux doesn't know about this stuff. In order to get Redux and React to communicate with each other, we are going to need to have something in the middle, namely something literally called react-redux. This is the NPM module we are going to install to in order to get React and Redux to communicate with each other because these things are not made out of the box to work with each other. Essentially, React-Redux will be in the middle to facilitate communication between Redux and React (something we could wire together ourselves with considerable overhead, but the community has already done this for us!). Inside of React-Redux we will get a Provider
component as well as connect
. The point is that there will now be some things in our application that will be entirely Redux-specific, entirely React-Redux-specific, and entirely React-specific:
Redux | react-redux | React |
---|---|---|
actions | Provider | state |
reducers | connect | components |
dispatchers | props |
Consider the following picture to illustrate things (explanation follows picture):
On the left-hand side we've got the store which is an entirely Redux thing, and we can put as much as we want in there or as little as we want in there. The store is going to be the place that basically any component can go to and either update the store (i.e., "publish" )or get stuff from the store (i.e., "subscribe"). That's the gist.
On the right-hand side we've got our React app, and our React apps have pretty much all been the same in structure (thanks to create-react-app
), where we have index.js
at the very top, App.js
, directly below it, and then everything else (i.e., our component structure tree).
Our goal is going to be to make it so any of the components in our React app can be able to talk to the Redux store whatever the components happen to be. And the way that we're going to do that (because Redux and React don't know anything about each other out of the box) is that the Provider
from React-Redux is going to know about the Redux store, and we are going to take the Provider
component and we're going to "bear-hug" the entire React application. And by "bear hug" we mean we are going to render the Provider
component inside of index.js
. So index.js
is going to have Provider
inside of it, and App.js
is going to have the Router
(i.e., BrowserRouter as Router
from react-router-dom
). So we're going to have both things. That way if any component inside of our React application needs to access something from the store, then it is already inside of the Provider
component so it will be able to go to the store, and it will do so by means of the connect
function which will be inside of the component subscribing to the store. So the connect
function, which is part of React-Redux will be inside of a component that needs to know about Redux, and because that component is inside of the Provider
it will be able to go to the store.
The main point in all of this is that React and Redux are not connected in any way by default. Both Redux and React have their own moving parts, and React-Redux is meant to bring these moving parts together in a unifying way. Why? Because we want our components in React to be able to go the store and grab state or update state from anywhere in the application in as seamless and trouble-free way as possible. So that's why this whole architecture exists. We're going to use Provider
and connect
from React-Redux in order to communicate between React and Redux.
How Redux works
Forget about React and React-Redux for a moment. We're just going to think about how Redux does its thing for a moment. Redux has the store. We've got an object inside the store, and that makes up our application state. It can be as big or as small as needed.
The way that Redux works is that the store is informed by a bunch of little functions (as many as you want). These little functions that inform the store are called reducers. So you have a bunch of little functions that return a piece of state. And what these functions return can be any data type, but the point is that they are returning a piece of state. Every one of them. The logic inside of these reducers can be simply or complex; whatever the case, all of the little reducers return an object and whatever they return is going to collectively make up the store. So the store itself--you don't put anything in there. We never mess with the store directly. We never update the store directly. Instead, we use the reducers. They are functions that return a piece of state, and then Redux will handle updating the store (similar to how this.setState
works in React--we never ever mess with state
directly in React; we never mess with the store directly in Redux--we use these reducers that return their own little pieces of state and all of those pieces of state aggragate or collectively make up the store). These reducers are not built into Redux--they are functions that we are going to write, that we are responsible for, and there are a couple of rules, and we are going to deal with those rules soon. But the reducers are just tiny functions that we write that return state and that state, all added together, makes up the store. Think of the store as a grocery store where the reducers are like departments: you need all of the departments (e.g., produce, fruits, etc.) individually to create an entire grocery store. So that's what the reducers are.
In order to update the reducers, we have what are called action creators, and we are going to write these as well. And action creators are also little functions. They are little functions that create what you might expect: actions! These are four big terms when it comes to Redux:
- Store
- Reducers
- Action creators
- Actions
An action is simply going to be a little object, and there are rules surrounding the actions, but at the absolute bare minimum, an action must have a property of type
. An action creator is a little function that makes an action object.
So reducers are little functions that return state that gets put in the store, and action creators are little functions that return actions (i.e., an object with at least a property of type
). That's really all there is to it even though the innerworking can obviously get much much more complex.
The last major piece of Redux is what's a called a "dispatch." And the dispatch is a function that is built into Redux--this one we don't make. We write the reducers entirely ourselves. We write the action creators ourselves and the actions as well. But we con't build the store, and we don't build the dispatch. The dispatch is given to us--it is part of the store. And what the dispatch does is it dispatches an action. The action creator will make an action, and when the action (i.e., an object with a type
property on it) gets triggered it will go to the dispatch, and the dispatch will take that action object and inform all of the reducers about it. It will let every reducer know, "Hey! Something happened." If our action were simply something like {type: 'auth'}
, then it would be, "Hey! I got an object with a type
of auth
. I don't know if you care, but I'm just letting you know." And then our reducers will kick into gear, and these reducer functions will need to decide if they care about this action or not. Do they care about the particular type
that is being passed on (you can already tell how reducer functions will often operate based on what type
an action has)? Ultimately, most actions will not only have a type
but also a payload
(e.g., some sort of data or whatever it might be that a reducer can work with if the reducer decides it cares about the type
of the action it is informed about). So what will happen is this: The action creator will fire off an action, that action will go to the dispatch, and then the dispatch will send the action to each reducer, and each reducer will have to make a decision about whether or not it cares about the action it is informed about (it makes this decision based on the type
property of the action object it receives), and if the reducers cares about the action, then it can grab the payload
and return a different piece of state. If it doesn't care about the action, then it can return the old piece of state. Whatever happens, all of the reducers will get notified every time an action is sent off by an action creator. And that is how the store is updated. The reducers return their little pieces of state. Those end up creating one big object in the store. Those individual little reducers will get notified every time an action happens by the dispatcher and then they can update themselves based on the payload
or whatever, but they'll always know what type of action it was because of the type
property on the action that is sent.
Wiring up Redux (all the gory details)
Here is a frame of reference for how everything is supposed to work (you do not have to follow these things in the given order, but it's how some people set things up anyway):
1. Use react-redux and wrap the Provider component around everything
import { Provider } from 'react-redux'
In order to wire up a Redux/React app, we need react-redux
. This is the glue between them. Specifically, we need the Provider
component to be around everything. So you will want to have the following in index.js
:
import { Provider } from 'react-redux';
So you're not just going to render <App />
like
ReactDOM.render(<App />, document.getElementById('root'));
in index.js
as we have normally done right out of the box. We're now going to render the Provider
component which is coming from react-redux
and that's going to "bear-hug" our entire React application. It's not going to stay this way (some more configuration is needed, as outlined below), but we can set it up this way:
ReactDOM.render(
<Provider>
<App />
</Provider>,
document.getElementById('root')
);
If we try to run things right away, then we will get a TypeError: Cannot read property 'getState' of undefined
error. The reason for this is because the Provider
component connects the Redux store with the React application. And right now we do not have a store! The above is trying to render a Provider
without a store and that's not okay.
2. Create the store
import { createStore } from 'redux'
Create the Redux store so that Redux exists and the Provider
has a store to work with:
import { createStore } from 'redux'
Hopefully this will help you remember where things are coming from: We've got react-redux
which is the module from which Provider
comes, and we've got the redux
module from which createStore
comes. If you see redux
as the module you're importing from, then that means React does not know about this thing, and it means react-redux
does not know about this thing. You could use this in any other framework or just in native JS. This is simply part of redux
. Now, we're going to give the store to the Provider
, but at this point import { createStore } from 'redux'
has nothing to do with anything--it's just pure Redux.
3. Make reducers to populate the store
What makes up the Redux store? If you remember, what makes up the Redux store are little functions that return a piece of state. What we need are reducers. So we need reducers to populate the store! The store by itself is just like an empty building. We need reducers to make the store useful. The store is just an aggregate of the reducers. We have to have at least one reducer in order to have any store. And the way that this part works is that we always start with a rootReducer
.
What that means is that we are going to make one sort of "master reducer", which may be thought of as the store manager, that will import all of the other reducers. Oftentimes you will have a reducers
folder inside of src
in your React application, and inside of reducers
the rootReducer
will often be simply called index.js
, but given how this name can be used all over the place, sometimes it is easiest to just name the rootReducer
as rootReducer.js
.
Inside of that file will be the rootReducer
. Now, to make a rootReducer
we will need to get a method from Redux called combineReducers
, which does exactly what its name implies. Each individual reducer will get combined together into one big store. We can get this method as follows:
import { combineReducers } from 'redux';
The next thing we will need to do is to actually get each individual reducer. So we'll need to make them and import them into the rootReducer
. So think back to what a reducer actually is. A reducer is just a function that returns a piece of state. The simplest reducer you might get would be something like
export default (state = [], action) => {
return state;
}
Note that all reducers have 2 parameters:
- Current state (usually provide a default state like the empty array above so if
state
is unset then we will initialize it as an empty array) - Info that came from any action
The reducer example given above is totally useless because it will always return an empty array. But it qualifies as a reducer because it's just a function. We could also write the above reducer using non ES6+ syntax (we will use the syntax above but the syntax below is worth knowing and being aware of):
function frozen(state = [], action) {
return state;
}
export default frozen;
To get the above reducer into the rootReducer
, we will call our imports in the rootReducer.js
file:
import frozenReducer from './frozen';
We now need to call combineReducers
and hand it an object. Each key in combineReducers
will be a piece of state in the Redux store, and each value will be a reducer, and that reducer will return a piece of state (so each value will be the value of that piece of state in the redux store). So for this example we can write something like the following:
const rootReducer = combineReducers({
frozen: frozenReducer
})
The above chunk of code means that in our Redux store we will have a little piece of state called frozen
and the value of frozen
is going to be whatever the frozenReducer
function returns. What does it return? Well right now it only ever returns state
where state
is an empty array so this is hardly useful, but hopefully the utility of all of this is starting to become apparent. But this is how Redux works is we will add another reducer for the produce and the produce will be another little piece of state inside of Redux. So, in this way, you can add as many new features, remove old ones, access any particular part of the Redux store by grabbing the return value of a little piece of state from the reducer.
From rootReducer.js
, once we have imported all of the reducers we need and combined them using combineReducers
, we should export default rootReducer
the rootReducer
we set up above.
We can then import the rootReducer
in the entry point of our application, index.js
:
import rootReducer from './reducers/rootReducer';
We are now actually ready to create the store by passing createStore
from the redux
module the rootReducer
we exported as the default export from rootReducer.js
and imported as rootReducer
in index.js
:
const theStore = createStore(rootReducer);
Since the Provider
is the glue between Redux and React, we need to use its store
prop and give it the value of theStore
we created above:
ReactDOM.render(
<Provider store={theStore}>
<App />
</Provider>,
document.getElementById('root')
);
Wiring up Redux (just the necessary pieces with comments)
After creating our React application using create-react-app
, we should do the following:
npm i redux react-redux
Then create a reducers
folder inside of the src
folder and make two files:
frozen.js
(this is a dummy reducer)rootReducer.js
(this will be therootReducer
; in other codebases you will often see this simply asindex.js
)
To get everything wired up after running create-react-app
and creating the two files above, our files should look as follows (comments provided to make things easier for reference):
index.js
(the top-level index that renders App
by default)
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// 1. In order to wire up a Redux/React application:
// - We need react-redux so that React and Redux can communicate with each other. We also need redux because react-redux will be useless without redux!
// - We need the Provider component to be around everything or "bear hug" the application. // - So you can start in the terminal with "npm i redux react-redux" and then import the Provider component:
import { Provider } from 'react-redux';
// 2. Create the redux store so that redux exists and the Provider has a store to work with:
import { createStore } from 'redux';
// 3. Provide/create individual reducers to populate the store. All of these reducers will be provided to the store as a single reducer known as rootReducer that may be thought of as the "store manager"
import rootReducer from './reducers/rootReducer';
// 4. Create the store by instantiating a variable with a value equal to the following: createStore(rootReducer):
const theStore = createStore(rootReducer);
// 5. Provider is the glue between React and Redux. Pass the store you created above as the value for Provider's store prop:
ReactDOM.render(
<Provider store={theStore}>
<App />
</Provider>,
document.getElementById('root')
);
rootReducer.js
// This is the root reducer! It is the store manager for all the reducers.
// To make a rootReducer:
// 1. Get a method from redux called combineReducers:
import { combineReducers } from 'redux';
// 2. Get each individual reducer:
import frozenReducer from './frozen';
// ... import all the other reducers here ...
// 3. Call combineReducers and hand it an object.
// - Each key in combineReducers will be a piece of state in the redux store.
// - Each value will be a reducer, and that reducer will return a piece of state (so each value will be the value of that piece of state in the redux store)
const rootReducer = combineReducers({
frozen: frozenReducer
})
export default rootReducer;
frozen.js
// This is just a dummy reducer!
export default (state = [], action) => {
return state;
}
// Below is the syntax one would use to avoid ES6+
// function frozen(state = [], action) {
// return state;
// }
// export default frozen;
That is all that is needed to get things wired up. Of course, nothing is really happening just yet (note how currently rootReducer
is purely Redux while any component we would normally make is purely React ... we need some way of CONNECTing these ... covered in the next note), but that's what all is needed to get the show on the road as it were.
CONNECTing Redux and React
We are now going to actually connect what is in our Redux store with an actual component (what good is state in the Redux store unless our components can actually use the state!?). So to recap where we ended previously:
We have the Provider
, and the Provider
has access to theStore
, where theStore
is made up of the rootReducer
, where the rootReducer
is made up of a bunch of little reducers, and each one of those little reducers returns a piece of state. So the rootReducer
is the manager of the store. (The store is like an empty building if it doesn't have any reducers.) The rootReducer
doesn't return any state on its own--it simply collects all the individual reducers. Continuing our metaphor, each individual reducer is like a department in a grocery store. In this case we have the frozen
department. So we have at least one department for the manager (i.e., the rootReducer
) to manage. And the manager can hand that to the store. Per our analogy, we currently have a store with just one department in it, the frozen
department, but we'll add some more departments in just a bit.
With everything as it currently is in the codebase described previously, we can create a FrozenDept
component (in a components
folder inside of the src
folder) that will connect to and make use of the Redux store (there are a bunch of comments in the file below; the next note seeks to condense things a bit):
FrozenDept.jsx
import React, { Component } from 'react';
/* Connecting this component with Redux
- This is the part now where we want to grab from the Redux store a piece of state. So this is that application state kind of thing, where we have a component, FrozenDept, and rather than the component managing the state itself, it's being managed inside of Redux. So ANY COMPONENT can grab it. This is not a practical example that follows because we only have a tiny application, which you would never use Redux for, but it will be useful to see how to finish wiring things up so that we can finish connecting Redux and React so that a component can get something out of the Redux store.
- We want this component to know about Redux. To do that, we need some help...or some glue.
- The glue is react-redux! We need the connect function. So we needed Provider up top in order to get everything started, but at the component-level what we need is a connection from React to Redux.
- If a component needs to know about Redux, then this is what we are going to use EVERY SINGLE TIME: import { connect } from 'react-redux';
- The export from a component will be a dead giveaway in terms of whether or not you are using Redux. You will not be exporting the default component anymore. Instead, you are going to export default the connect with the component
*/
import { connect } from 'react-redux';
class FrozenDept extends Component {
state = {
}
render() {
console.log(this.props.frozenData);
/* What we have managed to accomplish when the above works and reaches into the Redux store:
- If we go back to App.js, then we see we only call <FrozenDept /> and do not pass props as we normally might: <FrozenDept frozenData={} />
- Instead of the parent sending down frozenData as props, we are reaching into the Redux store via mapStateToProps and we're mapping a piece of state in the Redux store, namely "frozen" (whose state value is the value returned by the frozenReducer), to this component's props. Even though we only reached out to grab the "frozen" piece of state, we can easily imagine grabbing more than just this one piece of state.
- We have the "frozen" piece of state from the Redux store mapped to this component as a prop which we locally call "frozenData". This piece of state that has been mapped to this component as a prop can now be accessed using this.props.frozenData or whatever we choose to call this piece of state locally.
- "export default connect(mapStateToProps)(FrozenDept);" is the Redux part. What we are saying is let's set up a function, mapStateToProps, and go and fetch something from the Redux store, and then let's hand it to the component, FrozenDept.
*/
return (
<h1>The frozen food department!</h1>
);
}
}
// console.log(connect);
/* mapStateToProps (how it works):
- mapStateToProps takes one argument, "state", and that is the rootReducer/Store
- mapStateToProps returns an object with:
+ property is the local prop name to this component (so whatever we put on the left is what this component is going to know that thing as)
+ the value will be the property in the rootReducer, that is, a piece of the store
- We want to return an object and we need one of the properties of the rootReducer. What are the properties of the rootReducer? Well, we've only got one for this example and it is "frozen". So as far as connect is concerned we can return something, and we can call it anything we want--we're going to call it frozenData below.
- So we've got a function, mapStateToProps, which returns an object, where we are creating a local property, frozenData, to this component that is not being sent down from a parent--it's coming from connect, which is getting it from the store. What does the store have? Well, the store or the rootReducer IS state. What properties do we have as options right now? Well, we only have one right now, namely "frozen" so if we use state.frozen then we are going to get the return value of frozenReducer. What is the return value of frozenReducer? It will be state. What is state? Right now it's always going to be an empty array but not for long.
- RECAP: mapStateToProps takes state as its argument, where state is the rootReducer. The rootReducer has a property of frozen. We're going to map that state to this component's props, and that prop is going to be called frozenData.
*/
function mapStateToProps(state) {
return {
frozenData: state.frozen
}
}
// export default FrozenDept;
export default connect(mapStateToProps)(FrozenDept);
/* What's happening with connect:
- connect, somewhere inside of redux, is returning a function
- The function that connect returns takes an argument which is the component we want to render
*/
/* What connect take as arguments:
- We can find out by simply running console.log(connect) above.
- First arg: The first argument is going to map a piece of redux state to this component's props and is going to be called mapStateToProps
*/
/* If the above syntax looks funky, then try to make sense of something like x(1)(2) in JS:
function x(n) {
return (m) => {
console.log(n+m);
}
}
x(1)(2) // 3
*/
/* NOTES:
- Right now this may seem weird and not particularly useful. Why would anyone do this? We still need a bigger project to really make this whole thing shine. But everything above covers this portion of actually connecting React to Redux.
- Soon we will get an action creator and an action going that will actually give us the ability to update our state. (And we can add a couple more reducers.)
*/
The absolute most basic use of Redux (no action creator, action, or dispatch just yet)
Consider the following files that outline setting up the absolute most basic use of Redux where we fetch a piece of state from the store in a component (more things will be added soon, but right now we are trying to wrap our heads around the set up; each file's contents will then be remarked on).
index.js
: This is the entry point for our application and where we render something (typically just the App
component if we are not using Redux) to the DOM inside of <div id='root'></div>
in index.html
. With Redux, however, we want everything inside of our App
lication to be Provide
d access to the Redux store, but React and Redux don't know anything about each other so we have to connect them by means of the Provider
component from the react-redux
module. What Redux store are we trying to Provide
React access to? None unless we actually createStore
inside of Redux. What is a Redux store comprised or made of? A bunch of reducer functions that individually return tiny pieces of state, where all of the state pieces together collectively make up the store. Hence, createStore
should create the store by means of all of the reducers. Where do all of the reducers live? In the rootReducer
function from rootReducer.js
(remarked on more in a later point below). In any case, we create the store by means of a single master or root reducer that has every single reducer bundled inside of it. Once all of this is done, we can supply the Provider
component with the created store by means of the store
prop on the Provider
:
// /src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers/rootReducer';
const theStore = createStore(rootReducer);
ReactDOM.render(
<Provider store={theStore}>
<App />
</Provider>,
document.getElementById('root')
);
App.js
: This is the top component in our component tree (just below the entry point for our application, index.js
) under which all other components will live. In this basic example, all we want to do is use a component that tries to make use of Redux; thus, for right now at least, App
will look much as it has before:
// /src/App.js
import React from 'react';
import './App.css';
import FrozenDept from './components/FrozenDept/FrozenDept.component';
function App() {
return (
<div className="App">
<FrozenDept />
</div>
);
}
export default App;
frozen.js
: In order to not have a completely meaningless, empty store, we need to have at least one reducer that returns a piece of state to live in the store. Below is the absolute most basic reducer you could have--right now it is rather pointless because all it will do is return the default seed data we hand it (sometimes you may want to use a default value of an empty array []
or an empty object {}
or empty string ''
, etc.). But as we will see, you can initialize state in different reducers in helpful ways and often the body of the reducer will have a bunch (or a little) logic to run based on the type
of the action
it takes in. But for right now, we will simply have a default state value of some piddly seed data so we at least have something to work with:
// /src/reducers/frozen.js
const seedData = ['TV Dinners', 'Frozen Veggies', 'Frozen Pizzas'];
export default (state = seedData, action) => {
return state;
}
rootReducer.js
: When creating and providing the store in index.js
, we pondered what it actually meant to create the store. What is the store comprised or made of? We know the store consists of a bunch of pieces of state returned from a bunch of individual reducers. But how do we bundle together the pieces of state being returned by the reducers and the reducers themselves? Furthermore, where do the reducers actually live? Well, typically, the reducers will live in a "reducers" folder/directory. As for how we bundle together or combine all of the reducers in order to create the store, we make a root or master reducer whose sole job is to combine all of the reducers (sometimes people export the root reducer from index.js
in the reducers
directory, but we will export the root reducer from rootReducer.js
to make everything abundantly clear); specifically, we import the combineReducers
function from the redux
module, a function that expects a single object, an object that literally consists of key-value pairs where the key represents the piece of state being returned by an individual reducer and the value is the individual reducer itself. This may sound confusing at first, but it should make sense upon reflection: since the reducer returns a piece of state, essentially what we have is the key
as the name or label of what we want to call a piece of state being returned by a reducer and the value
for this key as the reducer itself since the reducer actually returns a piece of state (i.e., the value associated with the key should really not be thought of so much as the reducer itself but the return value of the reducer, namely the piece of state being returned; thus, whenever we reference the key, we are getting the value of the reducer associated with it). More concretely, below what we are effectively saying is, "Hey, the frozenReducer
reducer function returns a piece of state and I want the return value of this function (i.e., the piece of state) to be referenced as frozen
." As our application grows and we make more use of Redux, our rootReducer
will grow and grow as we dump more and more into the combineReducers
function, namely individual pieces of state (i.e., the value
which comes from the return value of the reducer we are calling) we can reference by name (i.e., the key
):
// /src/reducers/rootReducer.js
import { combineReducers } from 'redux';
import frozenReducer from './frozen';
const stateLabelsAndReducersReturningState = {
frozen: frozenReducer
}
const rootReducer = combineReducers(stateLabelsAndReducersReturningState);
export default rootReducer;
FrozenDept.component.jsx
(this file not will require more extensive commentary): This is the component that will use Redux for right now (more will be added to this component later and we will create additional components as well). How can we get this component to actually use Redux? We need some glue. We need some way of connecting Redux and React in this component and the connect
function from react-redux
is what we will use to accomplish this. We can actually take a look at the connect
function by inspecting the react-redux
node module:
node_modules -> react-redux -> lib -> connect -> connect.js
What we find is a lot of rather complicated code that is reproduced on click below.
connect.js
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.createConnect = createConnect;
exports["default"] = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
var _connectAdvanced = _interopRequireDefault(require("../components/connectAdvanced"));
var _shallowEqual = _interopRequireDefault(require("../utils/shallowEqual"));
var _mapDispatchToProps = _interopRequireDefault(require("./mapDispatchToProps"));
var _mapStateToProps = _interopRequireDefault(require("./mapStateToProps"));
var _mergeProps = _interopRequireDefault(require("./mergeProps"));
var _selectorFactory = _interopRequireDefault(require("./selectorFactory"));
/*
connect is a facade over connectAdvanced. It turns its args into a compatible
selectorFactory, which has the signature:
(dispatch, options) => (nextState, nextOwnProps) => nextFinalProps
connect passes its args to connectAdvanced as options, which will in turn pass them to
selectorFactory each time a Connect component instance is instantiated or hot reloaded.
selectorFactory returns a final props selector from its mapStateToProps,
mapStateToPropsFactories, mapDispatchToProps, mapDispatchToPropsFactories, mergeProps,
mergePropsFactories, and pure args.
The resulting final props selector is called by the Connect component instance whenever
it receives new props or store state.
*/
function match(arg, factories, name) {
for (var i = factories.length - 1; i >= 0; i--) {
var result = factories[i](arg);
if (result) return result;
}
return function (dispatch, options) {
throw new Error("Invalid value of type " + typeof arg + " for " + name + " argument when connecting component " + options.wrappedComponentName + ".");
};
}
function strictEqual(a, b) {
return a === b;
} // createConnect with default args builds the 'official' connect behavior. Calling it with
// different options opens up some testing and extensibility scenarios
function createConnect(_temp) {
var _ref = _temp === void 0 ? {} : _temp,
_ref$connectHOC = _ref.connectHOC,
connectHOC = _ref$connectHOC === void 0 ? _connectAdvanced["default"] : _ref$connectHOC,
_ref$mapStateToPropsF = _ref.mapStateToPropsFactories,
mapStateToPropsFactories = _ref$mapStateToPropsF === void 0 ? _mapStateToProps["default"] : _ref$mapStateToPropsF,
_ref$mapDispatchToPro = _ref.mapDispatchToPropsFactories,
mapDispatchToPropsFactories = _ref$mapDispatchToPro === void 0 ? _mapDispatchToProps["default"] : _ref$mapDispatchToPro,
_ref$mergePropsFactor = _ref.mergePropsFactories,
mergePropsFactories = _ref$mergePropsFactor === void 0 ? _mergeProps["default"] : _ref$mergePropsFactor,
_ref$selectorFactory = _ref.selectorFactory,
selectorFactory = _ref$selectorFactory === void 0 ? _selectorFactory["default"] : _ref$selectorFactory;
return function connect(mapStateToProps, mapDispatchToProps, mergeProps, _ref2) {
if (_ref2 === void 0) {
_ref2 = {};
}
var _ref3 = _ref2,
_ref3$pure = _ref3.pure,
pure = _ref3$pure === void 0 ? true : _ref3$pure,
_ref3$areStatesEqual = _ref3.areStatesEqual,
areStatesEqual = _ref3$areStatesEqual === void 0 ? strictEqual : _ref3$areStatesEqual,
_ref3$areOwnPropsEqua = _ref3.areOwnPropsEqual,
areOwnPropsEqual = _ref3$areOwnPropsEqua === void 0 ? _shallowEqual["default"] : _ref3$areOwnPropsEqua,
_ref3$areStatePropsEq = _ref3.areStatePropsEqual,
areStatePropsEqual = _ref3$areStatePropsEq === void 0 ? _shallowEqual["default"] : _ref3$areStatePropsEq,
_ref3$areMergedPropsE = _ref3.areMergedPropsEqual,
areMergedPropsEqual = _ref3$areMergedPropsE === void 0 ? _shallowEqual["default"] : _ref3$areMergedPropsE,
extraOptions = (0, _objectWithoutPropertiesLoose2["default"])(_ref3, ["pure", "areStatesEqual", "areOwnPropsEqual", "areStatePropsEqual", "areMergedPropsEqual"]);
var initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps');
var initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps');
var initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps');
return connectHOC(selectorFactory, (0, _extends2["default"])({
// used in error messages
methodName: 'connect',
// used to compute Connect's displayName from the wrapped component's displayName.
getDisplayName: function getDisplayName(name) {
return "Connect(" + name + ")";
},
// if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
shouldHandleStateChanges: Boolean(mapStateToProps),
// passed through to selectorFactory
initMapStateToProps: initMapStateToProps,
initMapDispatchToProps: initMapDispatchToProps,
initMergeProps: initMergeProps,
pure: pure,
areStatesEqual: areStatesEqual,
areOwnPropsEqual: areOwnPropsEqual,
areStatePropsEqual: areStatePropsEqual,
areMergedPropsEqual: areMergedPropsEqual
}, extraOptions));
};
}
var _default =
/*#__PURE__*/
createConnect();
exports["default"] = _default;
If you take a look at the contents of the file above, then what you will see is that _default
is being exported where var _default = createConnect();
. What is the return value of createConnect()
? It is the following:
return function connect(mapStateToProps, mapDispatchToProps, mergeProps, _ref2) { ... }
So the connect
function expects a few arguments even though only mapStateToProps
is required (we will remark more extensively on mapStateToProps
and mapDispatchToProps
, as these will be what are most often used). Worth noting, however, is the connect
itself returns a function:
return connectHOC(selectorFactory, ...);
For right now, we can simply read this as, "The connect
function expects a component to actually connect to the Redux store." But just because our component can be connected to the Redux store doesn't mean we are actually "subscribed" to the Redux store where we can pull state from it. Also, just because we are connected to the store doesn't mean we can "publish" to it. This is where mapStateToProps
and mapDispatchToProps
come in:
mapStateToProps
: This is how we will subscribe to the store and pull out from it whatever state we need or want to know about. Redux will literally map the application state (i.e., the state in the Redux store) to this component's props.mapDispatchToProps
: This is how we will publish to the store and update the state. We still have a way to go before adding the dispatch because we will need action creators and actions that reducers can use to actually update the store.
Here is what the basic component will look like momentarily:
// /src/components/FrozenDept/FrozenDept.component.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
class FrozenDept extends Component {
state = {};
render() {
console.log(this.props.localNameForStateFromRootReducer);
return <h1>The frozen food department!</h1>;
}
}
function mapStateToProps(state) {
return {
localNameForStateFromRootReducer: state.frozen,
};
}
export default connect(mapStateToProps)(FrozenDept);
Recap:
- Connect (i.e.,
import { connect } from 'react-redux';
):- We want this component to know about Redux. To do that, we need some help...or some glue.
- The glue is
react-redux
! We need theconnect
function. So we neededProvider
up top inindex.js
in order to get everything started, but at the component-level what we need is a connection from React to Redux. - If a component needs to know about Redux, then this is what we are going to use EVERY SINGLE TIME:
import { connect } from 'react-redux';
. - The export from a component will be a dead giveaway in terms of whether or not you are using Redux. You will not be exporting the default component anymore. Instead, you are going to export default the connect with the component:
export default connect(mapStateToProps)(FrozenDept);
- mapStateToProps
mapStateToProps
takes one argument,state
, and that represents everything inside of the Redux store (i.e., the entire object fed tocombineReducers
in the root reducer).mapStateToProps
returns an object of key-value pairs where the keys and values represent the following:key
: The local prop name to be used in the calling component. Whatever name is used is how this component will locally refer to a piece of state from the Redux store.value
:state.NAME
whereNAME
exists in therootReducer
as a key whose value is a piece of state being returned by a reducer.- Concrete illustration in the context of this component: The
mapStateToProps
function in this component brings instate
as its single argument and returns an object (this object only has one key-value pair right now, but in the future you may have many more). This object has a key oflocalNameForStateFromRootReducer
, and we should note that we can use whatever name makes sense. In this example, a better key might be something likefrozenData
. Regardless, what name we use as the key is how we will locally refer to this piece of application state from within our component (e.g.,this.props.localNameForStateFromRootReducer
). The value for this key needs to reflect what is in ourrootReducer
; specifically, in ourrootReducer
we have a key name offrozen
that points to the return value of thefrozenReducer
. Withfunction mapStateToProps(state) { ... }
, thestate
argument represents the store or the object fed tocombineReducers
in therootReducer
. Since that object has a key offrozen
whose value isfrozenReducer
, we can refer to this value or piece of state being returned fromfrozenReducer
viastate.frozen
.
- So we've got a function,
mapStateToProps
, which returns an object, where we are creating a local property,localNameForStateFromRootReducer
, to this component that is not being sent down from a parent--it's coming fromconnect
, which is getting it from the Redux store. What does the store have? Well, the store or therootReducer
IS application state. What properties do we have as options right now? Well, we only have one right now, namelyfrozen
--so if we usestate.frozen
then we are going to get the return value offrozenReducer
. What is the return value offrozenReducer
? It will be state. What is state? Right now it's always going to be theseedData
array we defined for default state (or an empty array if that is the default) but not for long. - SUMMARY:
mapStateToProps
takesstate
as its argument, wherestate
is therootReducer
. TherootReducer
has a property offrozen
. We're going to map that state to this component's props, and that prop is going to be called localNameForStateFromRootReducer.
- this.props.localNameForStateFromRootReducer
- If we go back to
App.js
, then we see we only call<FrozenDept />
and do not pass props as we normally might (e.g.,<FrozenDept frozenData={} />
). - Instead of the parent sending down
frozenData
as props, we are reaching into the Redux store viamapStateToProps
and we're mapping a piece of state in the Redux store, namelyfrozen
(whose state value is the value returned by thefrozenReducer
), to this component's props. Even though we only reached out to grab thefrozen
piece of state, we can easily imagine grabbing more than just this one piece of state. - We have the
frozen
piece of state from the Redux store mapped to this component as a prop which we locally calllocalNameForStateFromRootReducer
. This piece of state that has been mapped to this component as a prop can now be accessed usingthis.props.localNameForStateFromRootReducer
or whatever we choose to call this piece of state locally. export default connect(mapStateToProps)(FrozenDept);
is the Redux part. What we are saying is let's set up a function,mapStateToProps
, and go and fetch something from the Redux store, and then let's hand it to the component,FrozenDept
.
- If we go back to
Adding more reducers to our store
Before we take a look at action creators and the dispatch (because that tends to be the most confusing part), we are going to add a couple more reducers and get them into our application.
One thing to note about naming conventions: It is often not a bad idea to name your reducers <reducer-name>Reducer.js
. It just gets easier to track things. For example, instead of frozen.js
in our reducers
folder, we can rename this as frozenReducer.js
to make it abundantly clear.
Note that every time you add a reducer you have to go to the rootReducer
, import the new reducer(s), and then specify in the combineReducers
what you want the little piece of state being returned by the reducer to be called (what you have called this piece of state will be a new key, and the reducer itself will be the value for this key). For example, say we start with
import { combineReducers } from 'redux';
import frozenReducer from './frozenReducer';
const rootReducer = combineReducers({
frozen: frozenReducer
})
export default rootReducer;
as our rootReducer.js
file. Then, after writing two new reducers, produceReducer
and meatReducer
, we might want to have something like the following:
import { combineReducers } from 'redux';
import frozenReducer from './frozenReducer';
import produceReducer from './produceReducer';
import meatReducer from './meatReducer';
const rootReducer = combineReducers({
frozen: frozenReducer,
produce: produceReducer,
meat: meatReducer
})
export default rootReducer;
Note how the new reducers produceReducer
and meatReducer
have been imported, and we have called the pieces of state they return in combineReducers
as produce
and meat
, respectively. Then, in whatever component we want to access these new pieces of state we can update our mapStateToProps
:
function mapStateToProps(state) {
return {
frozenData: state.frozen
}
}
becomes
function mapStateToProps(state) {
return {
frozenData: state.frozen,
meatData: state.meat,
// only add in here what you want access to--so maybe we don't need access to the produceData
// produceData: state.produce
}
}
So this is the beauty of Redux. Any time new data is added to the store, any component can have access to it almost instantly. You just have to drop the one line into mapStateToProps
and suddenly it's accessible. Above, we started with just access to the frozen
piece of state from the store by accessing it as a prop (hence mapStateToProps
) and locally calling it frozenData
. But now maybe we want to access the meat
piece of state from the store. No problem. Just drop meatData: state.meat
into the mapStateToProps
function.
This gives us a ton of flexibility. No longer do we have to rearchitect our entire application by moving state up and creating callbacks at the appropriate level and so on. So this is a huge win.
Adding an action creator and action
We can start to see some of the power of Redux now, namely we do not have to rearchitect our entire application to let components know about different pieces of application state. But all of this is kind of pointless if the state is static. After all, the idea behind state implies some sort of dynamic value. We need to be able to update state for Redux to truly be useful. So the question now becomes: How do we update application state (i.e., the store) in Redux? In the context of what we have been doing so far: How do we update the quantity of the different food items? For example, we might want a plus/minus button for "TV Dinners" from the frozen
state mentioned previously to update the quantity (or we might want to be able to update the quantity for food from any department). In all cases, essentially what we are trying to do is update the state or the store.
Thus far, we have only interacted with the store and reducers via mapStateToProps
simply to get stuff from the store. But now we want to actually update the store. How do we do that? What is the store made of again? The store is made up of a bunch of pieces of state, all of which are returned by individual reducers. Hence, in order to change or update the store, we are going to need to change the reducers. And in order to change the reducers we have mapDispatchToProps
.
Recall that the connect
function from react-redux
can take more than just the mapStateToProps
function--it can also take a mapDispatchToProps
function. But what should the mapDispatchToProps
function look like and what does it do exactly? The answer behind this is where the increased complexity comes in. We are going to have to not only define a mapDispatchToProps
function, but we will need to make action creators as well as actions for reducers to receive in order for the dispatch
from mapDispatchToProps
to be of any use. The mapDispatchToProps
function, in its skeletal form, will look as follows:
function mapDispatchToProps(dispatch) {
return bindActionCreators({
// ...
}, dispatch)
}
Note the use of bindActionCreators
which comes from the redux
module (you will need to import { bindActionCreators } from 'redux';
at the top of your component). Basically, the dispatch
will take an action and will send it up to each individual reducer (the type
of the action is how the reducer will decide whether or not the action is of any importance). Actions, however, are created by action creators. For example:
export default (operation, index) => {
return {
type: 'updateMeat',
payload: {
operation,
index
}
}
}
Here, the function being exported is the action creator (which expects two arguments, namely operation
and index
) while the action itself is what the function returns:
{
type: 'updateMeat',
payload: {
operation,
index
}
}
Note from the above that action creators are just functions. We are now going to make a couple of action creators with a couple of actions. Often you will do this by creating an actions
folder inside of the src
folder for your application. Ultimately, what we really want to do is connect our action creator to our component and then send the action that the action creator returns on through the dispatcher so that all the reducers can get notified (this will be much clearer soon).
There's considerably more going on here! We will walk through all of the steps of actually adding the dispatcher and wiring everything up in the next note.
Adding the dispatcher
We are now going to connect the dispatcher to our components. We'll start with just using examples as they relate to everything "frozen".
We have the FrozenDept
component that uses Redux state:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import updateFrozen from '../../actions/frozenInvUpdate';
import { bindActionCreators } from 'redux';
class FrozenDept extends Component {
state = {};
increment = (operation, index) => {
this.props.updateFrozenInv(operation, index)
}
render() {
const { frozenData } = this.props;
const frozenInventory = frozenData.map((item, i) => (
<div key={i}>
<li>{item.food}: {item.quantity}</li>
<input type="button" onClick={() => this.increment('+', i)} value='+' />
<input type="button" onClick={() => this.increment('-', i)} value='-' />
</div>
))
return (
<div>
<h1>The frozen food department!</h1>
<ul>
{frozenInventory}
</ul>
</div>
)
}
}
function mapStateToProps(state) {
return {
frozenData: state.frozen
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
updateFrozenInv: updateFrozen
}, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(FrozenDept);
We've got frozenInvUpdate.js
which exports the action creator that the FrozenDept
component is going to want to run if the increment
function runs in our example:
export default (operation, index) => {
return {
type: 'updateFrozen',
payload: {
operation,
index
}
}
}
And the frozenReducer
is the reducer that returns the state inside of Redux that's informing the FrozenDept
component:
const seedData = [
{
food: 'TV Dinners',
quantity: 10
},
{
food: 'Frozen Veggies',
quantity: 21
},
{
food: 'Frozen Pizzas',
quantity: 25
}
];
export default (state = seedData, action) => {
// run a bunch of logic if the action has the type we are interested in
if (action.type === 'updateFrozen') {
const {operation, index} = action.payload;
const newState = [...state];
switch(operation) {
case '+':
newState[index].quantity++;
return newState;
case '-':
newState[index].quantity--;
return newState;
default:
return state;
}
}
// if the action does not have a type we are interested in, then just return state
return state;
}
This is where everything can get really confusing. There's just a lot going on here because we are attaching absolutely everything now. But soon we will know almost everything that we need to know about Redux with respect to React. There will hardly be anything else that you ever need to do. It's just going to be JS and logic and whatnot. But you'll know all of the moving parts necessary to make Redux work seamlessly with React.
Now, instead of just having a type
for the action you return from your action creator, you will often want to have a payload
to update the state in the Redux store. This is the most common object you will pass in Redux, an object with a type
and a payload
. You have to have a type
because we are dealing with an action and what a reducer does depends on the action type. And then the other thing you'll usually have is the payload
, and the payload just contains data or stuff that comes along with the particular type we specify. So you might see something like what we had above for an action creator:
export default (operation, index) => {
return {
type: 'updateFrozen',
payload: {
operation,
index
}
}
}
This is great, but we still do not have any connection to Redux. Let's go back to the diagram:
This is all Redux stuff. The FrozenDept
component has no idea that any of this stuff exists. React doesn't know about any of this stuff. React is running a function that has an action, but it's not connected to the dispatch in any way. So we do technically have the action creator and the action it is returning, but it's not really a part of Redux because right now it will never get to a reducer, and reducers make up the aggregate state in the store which is the whole point of Redux. What we need to do is make it so that the component can call the action creator, but we need to change it so that when we call updateFrozen
it goes to the dispatch. And then from the dispatch it will go to each reducer. That way the reducer can update itself, update the store, and then inside of our FrozenDept
component, where we are mapping through frozenData
from the Redux store, we'll be mapping through a new, updated thing, a new piece of state from the Redux store that has been updated by a reducer(s) which has been updated by a dispatcher(s) which was sent an action from an action creator.
We can do something like the following to see some of the innerworkings of Redux (this will run as soon as we fire off or start up our application):
export default (state = seedData, action) => {
console.log('Frozen reducer is running!');
console.log(action);
return state;
}
The actions we would see by default are the ones that Redux core runs and passes to the dispatch, something like the following:
{ type: "@@redux/PROBE_UNKNOWN_ACTION0.f.w.k.m" }
Now we don't care about these actions. But this is how the dispatcher works. We do care about our actions however. We want to make it so we can attach our action creators to the same dispatch Redux core uses so that all of our reducers can get notified and will update if they need to. So in our case we want to connect our updateFrozen
action creator to the dispatch.
How are we going to do this? Well, from before, recall what we got when we looked at the connect
function from the react-redux
module. We got something like the following:
connect(mapStateToProps, mapDispatchToProps, mergeProps, _ref2) { ... }
We have already experienced and used mapStateToProps
which allowed us to grab from any component any pieces of state we wanted from the Redux store as props. We will now focus on mapDispatchToProps
. And it's going to do exactly the same thing that mapStateToProps
did, where we have something like this.props.frozenData
which is mapped to the Redux store:
function mapStateToProps(state) {
return {
frozenData: state.frozen
}
}
We are about to make a this.props.updateFrozen
and we're going to map it to the dispatch (this is how we are going to connect the two--we are going to go from having a component that doesn't know about any of the Redux stuff to a component that can pass the function to the dispatcher which will notify each reducer):
Here are some notes about the default export
from a component when we are using Redux:
- Normal component export when using React:
export default FrozenDept;
- When applicable (state doesn't need to be updated in the store), just supply
mapStateToProps
as the only argument toconnect
:export default connect(mapStateToProps)(FrozenDept);
- When applicable (state DOES need to be updated in the store), supply both
mapStateToProps
as well asmapDispatchToProps
toconnect
in that order:export default connect(mapStateToProps, mapDispatchToProps)(FrozenDept);
The point is that now we can do this.props.updateFrozen(operation, index)
instead of updateFrozen(operation, index)
. And now the exact same code will run, BUT it will go to the dispatch. And because it goes to the dispatch, it will go to every single reducer. The difference from before when Redux core was doing things by default:
You can see how our type
and payload
will be as we specified previously, and we can see how this is sent to ALL of our reducers (we are using a console.log
in each reducer to specify that that reducer is running).
So what's the upshot here? Well, we're bringing it home. This is where the magic is finally going to happen. If we go to frozenReducer
, then we can see it is getting notified by the frozen action that, "Hey, I just happened. A type
of updateFrozen
just happened." And not only frozenReducer
but also the other reducers are getting notified as well. Every reducer gets notified when an action happens.
So in our frozenReducer
we can modify the code to look as follows:
export default (state = seedData, action) => {
console.log('Frozen reducer is running!');
console.log(action);
if (action.type === 'updateFrozen') {
console.log('I care about this action!');
return state
} else {
return state;
}
}
This code will run any time the dispatch sends an action over. (This will be true of all reducers.) Our code as specified is going to let us know that this code is running (the first console.log
tells us what reducer fires off) and what action just happened (the second console.log
). IF the action type equals updateFrozen
, then we are going to tell the console from the frozenReducer
that we care about that action and we are going to run some logic (to be filled in), and we are going to return state. But otherwise, if the action type
is not updateFrozen
, and it won't be on those first three actions that run by Redux core, then we are just going to return state (we won't try to update it in the store in any way). So all we need to do now is just run our logic based on what happens to update the state. We do not want to mutate state directly (just as in React when using this.setState
) so we will create a copy of the state, modify that copy, and then return that copy so that the state will be updated accordingly:
const seedData = [
{
food: 'TV Dinners',
quantity: 10
},
{
food: 'Frozen Veggies',
quantity: 21
},
{
food: 'Frozen Pizzas',
quantity: 25
}
];
export default (state = seedData, action) => {
// run a bunch of logic if the action has the type we are interested in
if (action.type === 'updateFrozen') {
const {operation, index} = action.payload;
const newState = [...state]; // make a copy of state and mutate the copy
switch(operation) {
case '+':
newState[index].quantity++;
return newState;
case '-':
newState[index].quantity--;
return newState;
default:
return state;
}
}
// if the action does not have a type we are interested in, then just return state
return state;
}
What's happening above? We create a copy of state
with const newState = [...state];
. Note that state
will start off as the seedData
specified at the top of the frozenReducer
file (this seedData
will often actually be data pulled from a database or something else). Because of how the updateFrozen
function works, we know what index we are after which is part of the payload
, and we can modify this part of the array (the quantity
) based on the operation
and the index
. We will then return newState
.
To summarize, IF we get the updateFrozen
action, and the only reason we would get it is because the frozenInvUpdate
action creator ran, and the only reason that action creator would run is because the FrozenDept
component called this.props.updateFrozen
, and it passed over an operation
and an index
.
So let's recap and look at the whole roundtrip: We've got an action (returned from the frozenInvUpdate
action creator) that's connected to the dispatch inside of our component. When we run that code, the dispatch kicks it off to all the reducers. The meatReducer
doesn't care. The produceReducer
doesn't care. Ah, but the frozenReducer
does care. So the meatReducer
and the produceReducer
will just return their old state, but the frozenReducer
will make a copy of state, it updates that state with some new data, it returns that to the store, and our FrozenDept
React component is looking at the Redux store, and it notices that, "Oh, something just changed. I had better re-render. What changed? The value of the frozenReducer
."
So what's the upshot of all this? Well, for our application, not a whole lot, but we have access to the entire frozen department everywhere in our application. So we can add any feature anytime anywhere and quickly patch in to that application state instead of passing callbacks all over the place.
Some review
Our application right now is set up like this: At the very top we have index.js
. That's not a component--that's where ReactDOM
renders. Inside of index.js
, the thing we are rendering immediately, unlike in the past, is no longer just <App />
. It is now the Provider
component (which comes from react-redux
), with store
as a prop, wrapped around our App
component.
We then have our <App />
component which branches into three components, the <FrozenDept />
, <MeatDept />
, and <ProduceDept />
. So at that point everything is React/JS with the exception of Provider
which is coming from react-redux
--we do not yet have anything that is purely Redux (until we involve the store that is).
Now, Redux has a store. And what we want is for our components to be able to get stuff from the store, and we also want them to be able to update the store. In order for this to happen, there are all of the other moving pieces that need to be in place. At this point it's not all too complicated. We have our component structure and one sort of odd component with Provider
and then the store is entirely external. The tricky part comes with the rest of Redux which is the dispatch and the reducers. Getting those two things to cooperate with each other is where most of the confusion seems to come in.
To actually make the store, we have the rootReducer
which is just Redux, and it imports a bunch of little reducers (the frozenReducer
, meatReducer
, and produceReducer
in our example). They are all just JS functions that all feed in to the rootReducer
. The rootReducer
is about to get passed in to the store, but right now we have three little reducers, each of them sort of like their own little department, just like a grocery store. All of them get tied together in rootReducer
. And back in index.js
, we grab the rootReducer
and make the store. So the store gets created from the rootReducer
, which is simply a combination of all the little reducers, and then we use that to build the store, and the store gets handed to the Provider
. So the Provider
now has the store.
So what's the store? It's the rootReducer
, which is just the return value of its composite reducer functions (i.e., the reducers). So this gets the store set up. In order to be able to actually use the store, we add connect
from react-redux
to every component that we want to know about Redux (in our case all of the components we had used connect
). Any component that is connect
ed can go to the store and grab whatever piece of state it wants. In our example, we are grabbing from the ProduceDept
component the produce
piece of state (or state.produce
which we are calling produceData
when we mapStateToProps
) which is returned by the produceReducer
which is part of the rootReducer
which makes up the store. The same thing is true for the MeatDept
component: we are grabbing the piece of state returned by meatReducer
which is part of the rootReducer
which makes up the store. And we are doing the same thing with the FrozenDept
component.
This gets us pretty much up to date with the lecture about connecting Redux and React. That's what we used conect
for (to let any REACT component know about REDUX so the component could use state from the store as props). The second part involved needing to update the state in store which involved needing to update the reducers (since each reducer returns a piece of state). In order to update the reducers, we need a special mechanism. Redux is all about application state. It manages the state of your application, and we want a very consistent, very predictable way of updating the state of the whole application. When Redux really starts to shine is when you have multiple components all trying to change the same thing at the same time. And then you start crossing wires and things start to go heywire. That's specifically what Redux is designed to help with.
connect
not only gives us access to the store but also gives us the ability to use the action creators (frozenInvUpdate
, meatInvUpdate
, and produceInvUpdate
in our example), and those action creators will pass up (i.e., return) to dispatch the action, where each action has a type
property and often a payload
property as well. All three of our action creators return actions, and we call those action creators with connect
, and because we map those functions to the dispatcher, when the function returns its value, it will get sent to the dispatch, and the dispatch is going to send those actions to every single reducer. The reducers make a decision about whether or not they should update themselves (i.e., create a new copy of the old state, modify it in some way based on the action type
, and then return it) or whether they should just return their old state. They return it to the rootReducer
, and the rootReducer
is what makes up the store. And because our components are looking at the store through connect
, they will get notified of any change and re-render accordingly to reflect the updated pieces of state from the store used as props within the component.
That is the whole cycle! And it is always the same. Why does it have to be so complicated and difficult? Redux is made to make the process as predictable and easy to manage as possible in the long run so that when you're troubleshooting a bug or you need to update a piece of state in some component, then it can be done in "the right way"; that is, it can be done in a very consistent, very predictable way in order to keep wires from getting crossed.
So next we are going to add the router which will work just as it has, where the Provider
is going to render App
and App
is going to render the router, and the router is going to render everything else.
Adding the Router
We are now going to add the React Router to our application. So we've already got Redux, which has pretty much taken over our index.js
. And while you could put the router around App
in index.js
, it's been Rob's experience that index.js
can get really crowded really fast if you have both the router and all of the Redux setup in the same file because both of them are going to require more and more code. So instead of adding React Router to index.js
, we are going to add it to App.js
. This is Rob's convention, but there are several ways to do this; regardless of how you do it, however, you never ever want more than one router.
So you can immediately do an npm i react-router-dom
and then import it in your App.js
file and wrap all of your application in the BrowserRouter
like so (along with using Route
to render the components we want to):
import React from 'react';
import './App.css';
import FrozenDept from './components/FrozenDept/FrozenDept';
import MeatDept from './components/MeatDept/MeatDept';
import ProduceDept from './components/ProduceDept/ProduceDept';
import { BrowserRouter as Router, Route } from 'react-router-dom';
function App() {
return (
<Router>
<div className="App">
<Route path="/frozen-dept" component={FrozenDept} />
<Route path="/meat-dept" component={MeatDept} />
<Route path="/produce-dept" component={ProduceDept} />
</div>
</Router>
);
}
export default App;
Now we won't see anything upon the application loading because no path currently matches what we have above, but we can fix that. With a NavBar
component: <Route path="/" component={NavBar} />
We want this to be present no matter where we are in our application. Note that the router is not interfering with Redux at all. It's inside of App
which is inside of the Provider
which has access to the Redux store. So these two are not in conflict at all.
We can then use Link
tags within the NavBar
to link to the appropriate paths. Why do we use Link
tags instead of a
tags? Because we do not want to leave the application. Then the reacte-router-dom
module will manage the JS that will just rewrite the URL instead of actually taking us away from the application. So it will update the DOM for us without taking us away.
So, in our example, we can count up and down and change quantities for different items, then navigate to another component using the Router, hop back, and our changes are still in effect. Note that going from one component to another means we are actually mounting and unmounting, popping components in and out of the main frame of App.js
, and when the component pops back in, it grabs the data it needs from Redux, and Redux, because it's outside of React, nothing changes. So this is really really handy.
Getting more from Redux (clearing all the inventory)
Suppose we wanted to see the entire grocery store and the quantity of food items in each department. Furthermore, and more importantly, supposed we wanted a way to clear all the inventory and set all the food quantities back to 0. How could we do this? Normally, without Redux, this would probably be rather painful. We would need access to all of the state from each food department and we would also need to pass down callbacks from each food department in order to make it possible for their state to change. Essentially, some serious rearchitecting of our application would be involved. And who wants to do that? But with Redux it's pretty simple (relatively)!
If we want to display the sum of all the quantities of food items in each food department, then we will certainly need to read from the store all the quantities for each food item in a department; that is, we will need mapStateToProps
:
function mapStateToProps(state) {
return {
frozenData: state.frozen,
produceData: state.produce,
meatData: state.meat,
};
}
Since we are also going to want to update the store (i.e., reset the inventory for all food departments), we are definitely going to need mapDispatchToProps
:
function mapDispatchToProps(dispatch) {
return bindActionCreators({
// ???
}, dispatch)
}
But what do we pass to bindActionCreators
inside of mapDispatchToProps
? Well, clearly an action creator of some sort, but what could we possibly use for our action creator here? It's actually quite simple: we just need an action creator with a type
of clearInventory
or some other message that all of the reducers can be on the lookout for to see if they need to clear their inventory. So the clearInventory
action creator can be as simple as the following:
export default () => {
return {
type: 'clearInventory'
}
}
Hence, our mapDispatchToProps
can look like the following:
function mapDispatchToProps(dispatch) {
return bindActionCreators({
clearInventory
}, dispatch)
}
We can now fire off the clearInventory
action creator from within our component by invoking this.props.clearInventory()
. Where should we invoke this? One very easy way would be to simply have a button
with an onClick
event handler and then call the action creator within the event handler:
<button onClick={this.clearInventory}>Reset All Inventory</button>
And clearInventory
would be a class method like this:
clearInventory = () => {
this.props.clearInventory()
}
But so far we haven't actually cleared or reset any inventory. We haven't spelled out how to update the store. How do we update the store? By means of reducers. How does everything we have done so far have anything to do with our reducers? Well, when the button referenced above is clicked, what happens? It triggers an action creator that gets sent to the dispatch and the dispatch sends the action returned by the action creator to all of the reducers. So what remains now is simply to add conditional logic to the reducers that should care about an action with type: 'clearInventory'
.
As an example, the frozerReducer
might be updated as follows (along with the other reducers that need to share this logic):
const seedData = [
{
food: 'TV Dinners',
quantity: 10
},
{
food: 'Frozen Veggies',
quantity: 21
},
{
food: 'Frozen Pizzas',
quantity: 25
}
];
export default (state = seedData, action) => {
// run a bunch of logic if the action has the type we are interested in
if (action.type === 'updateFrozen') {
const {operation, index} = action.payload;
const newState = [...state];
switch(operation) {
case '+':
newState[index].quantity++;
return newState;
case '-':
newState[index].quantity--;
return newState;
default:
return state;
}
} else if (action.type === 'clearInventory') {
const newState = [...state];
newState.forEach((foodItem) => {
foodItem.quantity = 0;
})
return newState
}
// if the action does not have a type we are interested in, then just return state
return state;
}
Notice how the only real addition was the following part:
else if (action.type === 'clearInventory') {
const newState = [...state];
newState.forEach((foodItem) => {
foodItem.quantity = 0;
})
return newState
}
This is super awesome because we might have an application where a single action effects 10 different reducers or even more. In this example, we just have one action (i.e., the clearInventory
one) that effects 3 reducers. But it quickly becomes apparent just how nifty this can be. This basically gives us the ability to add features however we want in our application with simplicity and ease. Need access to some application state? Throw in mapStateToProps
and grab what you need. Need to be able to update state? Throw in mapDispatchToProps
and whatever action you intend to effect different reducers (and then update the effected reducers).
We now have immense power over our application state from the very beginning to the maintenance phase of our software. Of course, the real question now becomes how do we do asynchronous things with Redux? Because that is where the true power will be unleashed (e.g., grabbing from databases, making API calls, etc.). We will look at asynchronous action creators next.
In summary, our Main
component just remarked on might look as follows:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import clearInventory from '../../actions/clearInventory';
class Main extends Component {
constructor(props) {
super(props);
this.state = {};
}
clearInventory = () => {
this.props.clearInventory()
}
render() {
const { frozenData, meatData, produceData } = this.props;
return (
<div>
<h1>Quantity of items in Frozen Department: {sumInventory(frozenData)}</h1>
<h1>Quantity of items in Meat Department: {sumInventory(meatData)}</h1>
<h1>Quantity of items in Produce Department: {sumInventory(produceData)}</h1>
<button onClick={this.clearInventory}>Reset All Inventory</button>
</div>
);
}
}
function sumInventory(foodDept) {
return foodDept.reduce((acc, foodItem) => {
return acc + foodItem.quantity;
}, 0)
}
function mapStateToProps(state) {
return {
frozenData: state.frozen,
produceData: state.produce,
meatData: state.meat,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
clearInventory
}, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Main);
Redux Middleware (asynchronous action creators!)
Introduction
What is middleware in Redux? If we think about it, what we have seen so far does not involve anything asynchronous; that is, all of our action creators thus far have sent a very simple action (like clearInventory
) or an action that had a payload that included some information supplied by an event triggered by the user (e.g., the increment
function that supplied an operation and an index). But what if we wanted to update the store with something that involved asynchronous work (e.g., pulling from a database, making an AJAX call, etc.)?
As the docs note concerning middleware, if you've used server-side libraries like Express, then you are already at least familiar with the concept of middleware. As they note: "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." The concept of middleware in server-side libraries makes complete sense--we'd like to have a way to patch into the process between receiving a request and sending a response so we could package together as good a response as possible based on what we think the user wants. The concept of middleware is similar in Redux except instead of patching into what goes on between receiving a request and sending a response on a server, we will instead patch into what goes on between dispatching or sending an action and the moment it reaches the reducer (i.e., before the reducer receives the action).
Think of it like this:
- Express:
receive request -> MIDDLEWARE -> send response
- Redux:
dispatch action -> MIDDLEWARE -> (reducer) receives action
The docs further note that people often use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, etc.
The basic point is that Redux middleware is absolutely critical to being able to use Redux with React; otherwise, you greatly limit the power of Redux.
Review of reducers, actions, action creators, and where we are headed now
First of all, we have React. Often we will use create-react-app
, and this will give us a ton of pre-loaded stuff to help us out. In particular, we have index.js
as the entry point to our application, and within index.js
we render the App
component, where App
turns around and renders whatever it needs to render.
On the other side, we now have Redux. And Redux is not related to React at all. They're totally separate and do not know about each other at all. And Redux is primarily this thing called the store, where state is managed. That is, one big object is maintained inside of that store, and the store is informed by individual functions each of which returns a piece of state. These individual functions are called reducers. Each one of these reducers is in charge of managing a piece of Redux state.
In order to get our React application to be able to talk with Redux, we had the react-redux
module as the middleman. This module gives us the Provider
component, and it also gave us the connect
function. And the Provider
component is rendered by index.js
:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import rootReducer from ''
import { Provider } from 'react-redux';
import {createStore} from 'redux';
const theStore = createStore(rootReducer);
ReactDOM.render(
<Provider store={theStore}>
<App />
</Provider>,
document.getElementById('root')
);
So index.js
grabs the Provider
component, and then the Provider
component bear-hugs the App
component so that everything within App
has access to all the Redux goodies. For any component that needs access to Redux, we export such a component with the connect
function. And because we use the connect
function, this gives us access to the Redux store. So Provider
makes it possible for our components to connect to the store, and invoking the connect
function with our component activates this possible connection. Hence, essentially, a component can subscribe to things that are happening in the store via connect
. For example, via mapStateToProps
, our component can subscribe to a piece of state or several pieces of state from the Redux store so that any time something happens inside of Redux, our component will get notified and rerendered with the new prop (remember that React will always rerender a component when it receives new state or new props; since Redux gives us state from the store as props
via mapStateToProps
, any piece of state that changes in the Redux store to which our component is subscribed effectively causes a rerender of the subscribed component because the state change in Redux is registered as a component prop change in React). All of this happens through mapStateToProps
which is passed to connect
.
The difficult and somewhat confusing part arises when we try to publish to the store (i.e., when we try to update state in the store). The wiring up for all of this is rather involved (i.e., making action creators, specifying what type
of action is returned and the payload
if applicable, modifying appropriate reducers to listen for relevant type
s of actions, and then using mapDispatchToProps
to bring in the relevant action creators we want access to). An action creator, just like a reducer, is nothing more than a regular JavaScript function. And action creators return actions, where an action must be an object with at least a type
property. So the action creator is the function itself, and the action is the object that the function returns. Of course, we don't write them this way out of the box--we write them as regular JavaScript functions. Our action creators really aren't action creators until we had them to bindActionCreators
from the redux
module. Recall the example when clearing the food department inventory:
function mapDispatchToProps(dispatch) {
return bindActionCreators({
clearInventory
}, dispatch)
}
This is something we do with all of our actions; that is, any time we have a mapDispatchToProps
, we first run our action creators through bindActionCreators
. That is what takes our action creators from being regular JavaScript functions to being effective action creators that Redux can use.
The other thing we did was we ran our action creators through connect
by sending them through mapDispatchToProps
:
export default connect(mapStateToProps, mapDispatchToProps)(Main);
So the action creators go via connect
and can be called by our component via this.props.clearInventory()
. So we can initiate our action creator function from our component through connect
and because we mapped our action creator to the dispatch
, once the action creator has returned its action, the action is going to get sent to the dispatch
, and then the dispatch
takes the action object returned from the action creator and sends it out to all of the reducers. It notifies all of them. The reducers then decide whether or not they want to update, and then they send their information back to the store.
That is the basic review for all of the moving pieces that go into a React application that uses Redux. We have the glue in the middle (i.e., react-redux
) that allows React and Redux to communicate. Our React components work as they always have, but since we run them through connect
from react-redux
, they're able to grab pieces of state out of the Redux store, and they're able to run action creators that will run through the dispatch when they return their action, and then the store will get updated.
We can simplify this a bit down by just focusing on the Redux part. We've got the store, and each individual reducer informs the store via an individual returned piece of state. And somehow one of our components has called one of our action creators which is just a regular function that returns an object, and that object is going to get sent to the dispatch
, and the dispatch
is going to let all the individual reducers know that that action happened and that they can decide to update or not. Meanwhile, any component that has subscribed to the store (or listening to a piece of state) will get notified if and when something happens to the piece of state to which it is subscribed. So the action creator gets called, the action gets returned to the dispatch
, the dispatch
informs each reducer, the reducers decide whether to update themselves or not but either way always return some piece of state, and then the components that are subscribed get updated if there is any update to the piece of state to which they are subscribed.
And here is where middleware comes into play. There are times when we are going to want to interrupt or jump in the middle between when the dispatch
receives the action and when the action gets sent to the reducers. So we have an action that has been dispatched, and we want to do something before it actually gets to the reducers. Something needs to happen in that process. The most obvious example of when you might want to jump in the middle of this process is when you are making an API call or some sort of asynchronous request where you need to wait for something momentarily. So let's say our action creator needs to fetch something from the web. So our action creator needs to make an axios request, and an axios request automatically returns a promise. So the question is whether or not we can do something after the axios request to inform the payload of the action that then gets sent to all of the reducers.
Trying axios/http without middleware
In the set up for the codebase, the crux of everything is the fetchWeather
action creator. It may be tempting to try to do something like the following:
import axios from 'axios';
const weatherApi = `http://api.openweathermap.org/data/2.5/weather`;
const weatherAPIkey = `6f3f23c0f1a2fcb7edee25d08cb9cf62`;
const scale = `imperial`;
export default async (city) => {
const weatherURL = `${weatherApi}?q=${city}&units=${scale}&appid=${weatherAPIkey}`;
console.log(city);
console.log(weatherURL);
const response = await axios.get(weatherURL);
return {
type: 'cityUpdate',
payload: response.data
}
}
But no dice. If we try this, then React will shoot us back an error:
Error: Actions must be plain objects. Use custom middleware for async actions.
So we can't return something that is not an object. But it looks like we are! But Redux runs synchronously, meaning it immediately sends the action to the dispatch, and then the dispatch immediately sends the action to the reducers. So we have to somehow stop the process here. We have to do something to cut this off and then dispatch the action when we are ready. And that is exactly where custom middleware comes into the picture.
Async action creators with redux-promise
Let's revisit where we left off previously, namely our action creator that failed because it was trying to do something asynchronous:
import axios from 'axios';
const weatherApi = `http://api.openweathermap.org/data/2.5/weather`;
const weatherAPIkey = `6f3f23c0f1a2fcb7edee25d08cb9cf62`;
const scale = `imperial`;
export default async (city) => {
const weatherURL = `${weatherApi}?q=${city}&units=${scale}&appid=${weatherAPIkey}`;
console.log(city);
console.log(weatherURL);
const response = await axios.get(weatherURL);
return {
type: 'cityUpdate',
payload: response.data
}
}
The type
for the returned action is fine--it is simply a string. The problem is with the payload
which is a promise. That's what response.data
is above. And Redux is made to work as automatically as possible. Redux works synchronously and expects all actions to be dispatched and processed immediately, but our action above can't be because the payload
is a promise. It won't run until the promise has settled.
In order to solve this problem, we need to use some middleware. For this specific example right now, the middleware we will use comes from the redux-promise module. This module is incredibly useful in that you don't have to write much code at all to make things work, but the downside is that you don't really have anymore control over things (that's where something like redux-thunk
might come into play or redux-saga
).
The docs for redux-promise
makes its usage fairly clear: "The default export is a middleware function. If it receives a promise, it will dispatch the resolved value of the promise. It will not dispatch anything if the promise rejects."
In order to apply middleware from redux-promise
, we need to go all the way back up to index.js
where the store is created because that is where the middleware will be applied to the store. So instead of just
import { createStore } from 'redux';
we will now have
import { createStore, applyMiddleware } from 'redux';
where applyMiddleware
is coming entirely from redux
. How can we actually apply the middleware from redux-promise
? Like so:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import rootReducer from './reducers/rootReducer';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reduxPromise from 'redux-promise';
const theStore = applyMiddleware(reduxPromise)(createStore)(rootReducer);
ReactDOM.render(
<Provider store={theStore}>
<App />
</Provider>,
document.getElementById('root')
);
The line
const theStore = applyMiddleware(reduxPromise)(createStore)(rootReducer);
looks rather funky. What is going on here? Well, the docs for redux-promise
tell us that the default export from redux-promise
(which we are calling reduxPromise
) is a middleware function. So basically applyMiddleware(reduxPromise)
is just another function expecting another argument, and the argument we want to pass it in this case is createStore
. Well, createStore
itself is a function so basically applyMiddleware(reduxPromise)(createStore)
returns a function that expects the rootReducer
, and that is what all is happening above. A more cumbersome albeit perhaps slightly clearer way of writing it would be as follows:
const middlewareApplied = applyMiddleware(reduxPromise);
const storeWithMiddleware = middlewareApplied(createStore);
const finalStore = storeWithMiddleware(rootReducer);
We will stick with the first way since it is less wordy. The effect of the line
const theStore = applyMiddleware(reduxPromise)(createStore)(rootReducer);
is that a variable called theStore
is created that already has our middleware applied to it, where this solves our basic problem.
Async action creators with redux-thunk
We are now going to look at our next piece of middleware: redux-thunk. This is sort of "Step 2" of the middleware process. The redux-promise
module is nice but it gives us very little power or control over things.
Redux thunk is going to be more manual than redux-promise
but the catch is that we will have more control.
The authors of the redux-thunk
package give us somewhat of a mean message at the outset: "Why do I need this? If you're not sure whether you need it, you probably don't. Read this for an in-depth introduction to thunks in Redux." This may be kind of annoying but it conveys a true message, namely that you shouldn't try to force this middleware because most of the time you don't need it. But just remember you have it in your back pocket so that when you do get in a situation where you need more direct control over the dispatch or access to the store then you can get it.
Here's the key part with redux-thunk
: "Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch
and getState
as parameters."
This is very different from everything we have been doing inside of our action creators so far:
import axios from 'axios';
const weatherApi = `http://api.openweathermap.org/data/2.5/weather`;
const weatherAPIkey = `6f3f23c0f1a2fcb7edee25d08cb9cf62`;
const scale = `imperial`;
export default async (city) => {
const weatherURL = `${weatherApi}?q=${city}&units=${scale}&appid=${weatherAPIkey}`;
console.log(city);
console.log(weatherURL);
const response = await axios.get(weatherURL);
return {
type: 'cityUpdate',
payload: response.data
}
}
We have always been returning an object from our action creator and never a function. What redux-thunk
is promising us is that we can take our action creators and instead of returning an action/object, we can return a function. So instead of doing return { ... }
we can do return () => {}
. Now why would we want to do this? Well, remember, if you don't know the answer to that question, then you probably don't need redux-thunk
. But we will see how we can do it anyway!
Sometimes the name of the module can help us out. redux-promise
gave us an indicate that a promise
would be involved in some way. Well, redux-thunk
gives some indication that a thunk
will be involved in some way. But what the hell is a thunk? The docs remark on this: "A thunk is a function that wraps an expression to delay its evaluation." And they give an example to illustrate this:
// calculation of 1 + 2 is immediate
// x === 3
let x = 1 + 2;
// calculation of 1 + 2 is delayed
// foo can be called later to perform the calculation
// foo is a thunk!
let foo = () => 1 + 2;
So how can we start to use redux-thunk
? Do we need to get rid of redux-promise
? No! And this is the really cool part--we can use multiple pieces of middleware in Redux. In index.js
we can now have the following:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import rootReducer from './reducers/rootReducer';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reduxPromise from 'redux-promise';
import reduxThunk from 'redux-thunk';
const theStore = applyMiddleware(reduxPromise, reduxThunk)(createStore)(rootReducer);
ReactDOM.render(
<Provider store={theStore}>
<App />
</Provider>,
document.getElementById('root')
);
So how do we actually use redux-thunk
now and what's the value? The value comes from the fact that the function we return from our action creator is given two arguments, the dispatch
and getState
. So far we have not interact with either of these things at all. It has been entirely react-redux
's job to call the dispatch
. It has been react-redux
's job to actually fiddle with the store and grab state out. But with redux-thunk
we get the power to interact with these objects (below is just a temporary dummy example):
export default () => {
return (dispatch, getState) => {
setTimeout(() => {
console.log('I waited for 2 seconds')
dispatch({
type: 'testThunk'
})
}, 2000)
}
}
The above code snippet shows that, with a thunk, we can manually call the dispatch and pass it an action of our choosing. The action right now is just a silly one with a type
of 'testThunk'
, but you can imagine hown this might be useful. The effect of the code above is that the action creator waits two seconds before sending out the action { type: 'testThunk' }
. So we now have the power to manually call the dispatch to send out an action of our choosing to all reducers.
The other option we have is getState
. So we can modify our action creator like so:
export default () => {
return (dispatch, getState) => {
setTimeout(() => {
const reduxState = getState();
console.log('Redux state: ', reduxState)
console.log('I waited for 2 seconds')
dispatch({
type: 'testThunk'
})
}, 2000)
}
}
The effect of this is that when our action creator gets called, we'll return a function where setTimeout
will wait two seconds, it will fetch the entire Redux store/state, put it inside of the reduxState
variable, log it, and then dispatch our action.
So we have access to both the entire Redux store and the dispatch inside of our thunk. What's the value of getState
here? Well, there might be things going on in your application that you need to know before you actually submit your action to go to all of your reducers. For example, suppose there is a component that is dependent on the user already having grabbed the weather in order to actually update it. Well, we can check to see if something exists and then and only then call the dispatch:
export default () => {
return (dispatch, getState) => {
setTimeout(() => {
const reduxState = getState();
console.log('Redux state: ', reduxState)
console.log('I waited for 2 seconds')
if (reduxState.weather.main) {
dispatch({
type: 'testThunk'
})
}
}, 2000)
}
}
The if
statement above is kind of silly, but the concept is what is important. Maybe it's a very important conditional that we need to consider. Maybe we only want to dispatch only if the store has certain information like authentication details or whatever else there might be. Or it could be that we want to dispatch four different actions all with different information all reliant on something else (you can imagine you could have conditional logic based on Redux state where we dispatch different actions based on what is in the store).
So we could have something like the following (not the best example but gets the point across):
import axios from 'axios';
const weatherApi = `http://api.openweathermap.org/data/2.5/weather`;
const weatherAPIkey = `6f3f23c0f1a2fcb7edee25d08cb9cf62`;
const scale = `imperial`;
export default (city) => {
return async (dispatch, getState) => {
let weatherURL = `${weatherApi}?q=${city}&units=${scale}&appid=${weatherAPIkey}`;
const reduxState = getState();
if (!reduxState.weather.main) {
weatherURL = `${weatherApi}?q=London&units=${scale}&appid=${weatherAPIkey}`
const response = await axios.get(weatherURL);
dispatch({
type: 'cityUpdate',
payload: response.data
})
} else if (reduxState.weather.main) {
const response = await axios.get(weatherURL);
if (reduxState.weather.id !== response.data.id) {
dispatch({
type: 'cityUpdate',
payload: response.data
})
}
}
}
}
The important thing here is that the middleware gives us the power to run the thunk function (the example above illustrates how we will only send the dispatch if we get a new city). If we go back over to our index.js
and remove reduxThink
from our middleware, then we got the error we got previously:
Error: Actions must be plain objects. Use custom middleware for async actions.
Why this error? Because we have in our Weather
component the following:
componentDidMount() {
this.props.testThunk()
}
And what does the testThunk
action creator try to do? It tries to return a function as opposed to a plain object. The reduxThunk
middleware gives us the ability to return functions where we can manually call dispatch
and getState
before returning an action object.
So redux-thunk
is actually very powerful because when you need control inside of an action creator you can get it. As noted above, maybe there are situations where you do not want an action dispatched from an action creator. redux-thunk
gives us all the power to do this. Our thunk is very powerful because we can run whatever code we want inside of it before deciding on whether or not we should dispatch an action to all of our reducers.
Before Hooks
Introduction to hooks and whether or not you should use them
As the docs say, "Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class." They say basically the same thing in the React changelog where we can see Hooks were added on February 6, 2019.
So what does it mean to use state and other React features without writing a class? Well, up until now, every time we have wanted to use state, the word this
for any reason, or any of the component lifecycle methods (e.g., componentDidMount
, componentDidUpdate
, render
, etc.), then we had to use a class. Functional components were only useful for presentational components. They never managed their own state. The only thing they had was props. Hooks seeks to give you the option of doing it either way (i.e., using state and React features in classes or in functions).
Before going deep into things, consider the following very simple class-based version of a simple counter:
class App extends React.Component {
state = { counter: 0 }
updateCounter = () => {
this.setState((prevState, props) => ({
counter: prevState.counter + 1
}));
}
render() {
return(
<div>
<div>Counter: {this.state.counter}</div>
<button onClick={this.updateCounter}>Add One!</button>
</div>
)
}
}
Nothing shocking here. Very straightforward. Now consider the following equivalent hooks-based version of the same counter:
import React, {useState} from 'react';
import './App.css';
function App() {
const [counter, setCounter] = useState(0);
return(
<div>
<div>Counter: {counter}</div>
<button onClick={() => setCounter(counter + 1)}>Add One!</button>
</div>
)
}
We're doing the exact same thing we were doing before, but now we are doing it without the overhead of a class. It's definitely shorter, less clunky, fewer extra variables, etc., in order to accomplish the same thing with the new system. A couple comments though:
- The docs remark on how there are no breaking changes with hooks.
- Completely opt-in (you can try Hooks without rewriting any existing code): Hooks are the future of React but there's no reason to feel like you have to use them.
- 100% backwards-compatibile (no breaking changes): So you can use hooks in a class-based application without any problems.
- Available as of v16.8.0
- There are no plans to remove classes from React: This is a huge line. Very important. They tell us we can read more about the gradual adoption strategy for Hooks in this section. The basic point is that this is a dual strategy by Facebook (by the React team)--they are not planning on getting rid of classes. So you can use both at the moment. That may change eventually, but there are no plans currently to move away from classes. This is important to keep in mind because if you are a new developer, then a lot of times you want to overcommit to whatever the cool/new/shiny thing is, and that's not a good idea. It doesn't mean don't use hooks, but it means don't completely overcommit to hooks and forget about classes forever. You need a good reason to go without classes; otherwise, you'll end up in a bad place later on. The industry right now remains object-oriented focused. Whether or not you like it, Java and C# still dominate the software engineering industry and they are class-based and object-oriented.
- Hooks don't replace your knowledge of React concepts: "Instead, Hooks provide a more direct API to the React concepts you already know: props, state, context, refs, and lifecycle. As we will show later, Hooks also offer a new powerful way to combine them." This should be good news! All of our hard work with classes will not be for waste at all. If anything, knowing hooks will simply be another powerful tool in our React toolbelt. So React is the same--it's just, "Hey! Here's a new tool." Hooks do not give you new power that you did not already have. It's just a different way to organize your code. It's so difficult to accept that if you are learning a new thing. It's tempting to abandon classes and go straight to hooks, but that is a bad idea. Don't do that! Any large existing codebase is going to be all classes right now. As the React team notes in the general adoption strategy section, "Crucially, Hooks work side-by-side with existing code so you can adopt them gradually. There is no rush to migrate to Hooks. We recommend avoiding any “big rewrites”, especially for existing, complex class components. It takes a bit of a mindshift to start “thinking in Hooks”. In our experience, it's best to practice using Hooks in new and non-critical components first, and ensure that everybody on your team feels comfortable with them." They go on, "We intend for Hooks to cover all existing use cases for classes, but we will keep supporting class components for the foreseeable future. At Facebook, we have tens of thousands of components written as classes, and we have absolutely no plans to rewrite them. Instead, we are starting to use Hooks in the new code side by side with classes."
- At the end of the point above, Facebook notes that they have thousands of class-based components and they have no intention of rewriting them with hooks. If that's Facebook's position, then you have to take them seriously. You do not have to go to hooks. They are simply a new tool that can unleash potentially new power in your applications.
- The overall point is that you should not be totally overcommitting and selling out to hooks forgetting about classes and abandoning them completely. Similarly, you should not remain stuck in the mud refusing to learn hooks and only stay with classes. Both hooks and classes are absolutely essential if you are going to be a React developer. Hooks are the intended future of the framework, but classes remain the tried and true way of going about wiring together large applications and they have been used extensively since ES6+. They both do the same thing. They're just two different tools. Don't just carry a hammer. Don't just carry a screwdriver. Carry a hammer and a screwdriver and use the one that you need.
Using React to explain OOP and functional programming
We are now going to talk about programming paradigms, specificially object-oriented programming (OOP) and functional programming (FP), and we will use React as the landscape in which we discuss the different defining concepts of each paradigm. The reason this is important to talk about in the context of choosing whether or not to use class-based components or function-based components with state (i.e., hooks) is because that is precisely what we are dealing with here. We have classes and we have hooks, and they are in direct competition with each other. They do exactly the same thing. You can use them together, but all that means is you can have a class that has hooks in it, but they do the same thing in that one is managing state at an object level (i.e., classes) and the other is managing it at a functional level (i.e., hooks). And those are two completely different things. The difference between the two is entirely organizational. It is how you organize your code. It does not change what you code does, it changes how your code is organized. So a programming paradigm is for the developer, and you have one whether you know it or not, but it's for the developer not for the machine. The overall point is that classes and hooks are all about how you organize your code. As the React team noted, hooks do not replace any of your existing knowledge--they simply make some of the API access easier and more straightforward. Two of the main programming paradigms are OOP and FP. We will now discuss them in the context of React. Classes use OOP while Hooks use FP. We'll start with classes and OOP since that is what we have been using up until now.
OOP follows a couple of tenants--there are 4 pillars of OOP:
- Encapsulation: This is just a fancy word that means we are going to take all of the data that belongs together and all of the functions that change or mutate that data will be in the same thing. So if you want to change whatever data there is (e.g., an array, an object, etc.), you have to use a method or a function and it has to belong to "me". So the object contains everything. That is exactly how our code has worked so far. For example, we never set state directly--we used an internal class method,
this.setState
, in order to change stuff because the component needs to be encapsulated. We want the data of the object or component (e.g.,this.state.counter
in the previous note) to be managed by the object or component, and we want to do that as much as possible. So we do mutate state, but we do it with setters (e.g.,setState
) and getters specific to the class we are working with. But OOP mutates state on purpose--it just does it in a controlled way. - Abstraction: The idea of abstraction is that we want to hide everything that we possibly can from the outside world and only expose what we absolutely have to. The components work this way in React because
this.WHATEVER
can't be seen by anybody else because it belongs to that object or component so if we make athis.updateCounter
orthis.changeName
or something like that, then no other component can use that or even see the state inside of it. Nobody can use those methods and nobody can change that data unless we pass those methods around. So as a quick example, if we have a component that renders another component (e.g.,App
renderingCounter
), thenApp
can have all of its own stuff andCounter
can have all of its own stuff, and they do not see each other, but if we wantCounter
to be able to use or see something inside ofApp
, then we pass this something down as a prop toCounter
. That prop can show data and it can enable functions. So we can run code inside ofCounter
that's actually written inside ofApp
. In the same way, we can access variables inside ofCounter
that are actually written inside ofApp
. But all of the variables that we see inCounter
that are coming fromApp
...we can't change them. We can only see the values of them. The code we can run inside ofCounter
that can change the variables inApp
still comes fromApp
. So basically you're given an outside look intoApp
withinCounter
, butApp
is still changing itself.Counter
cannot change anything inApp
.Counter
can only tellApp
to change itself. So, again, there's mutation going on. We're changing variables inside of objects, but we're hiding as much as possible from everybody else in order to keep the encapsulation principle as alive as possible. - Inheritance: We won't spend much time on this since JavaScript is not an inheritance-based language but a prototype-based language (i.e., JavaScript doesn't actually support real inheritance). But the basic idea is that you can take an existing class and make a new class with pretty much everything that the main one already has. We used this with the
React
object from thereact
module when we did something likeclass MyComponent extends React.Component
. So we got all of the super class (i.e.,React.Component
) stuff inside ofMyComponent
by callingsuper
within our own classconstructor
method. So we getcomponentDidMount
,componentDidUpdate
,render
, etc. It's helpful to do this because you can customize and reuse existing code. - Polymorphism: This may sound scary but it's really not a big deal at all. Basically, polymorphism means that if you extend a class, like extending
React.Component
into our ownMyComponent
class, then if we want to use the same method fromReact.Component
, then we have the ability to overwrite it. We actually do this all the time becausecomponentDidMount
is defined inside ofReact.Component
--we just define it sometimes inMyComponent
. If we don't define it, then we're using the one thatReact.Component
has made for us. If we overwrite it, then we're not using theReact.Component
one anymore--we're using our method. That's all polymorphism is. And we have to use it because if we want to our component to be customizable from the extended class, which in this case isReact.Component
, then we have to use polymorphism.
This is a super brief overview of OOP in the context of React, but the main thing here is that we are keeping everything internal (encapsulation), we're trying to hide everything we can from the outside world (abstraction), we get to use stuff from other classes like React.Component
(inheritance), and we can overwrite stuff in React.Component
whenever we want to (polymorphism). Basically what we are doing with all of this is MUTATING state but doing so in a very controlled way. But we're changing state constantly in an OOP approach. But it's okay because we are following the precepts outlined above to make sense out of everything.
For FP, there are two main pillars or tenets:
- Purity: This means that we do not mutate state. There is no side effect ever of running a pure function. Given a piece of input, a pure function should always return the same output. That's not true in OOP because state might be different. If state is different, then given a piece of input, we could get any number of outputs because it depends on what the state of the program is. In a pure function, that is never true; that is, if you give a piece of input, then you will always get the same output.
- Immutability: We still have state. We just don't ever change it. We simply replace it. So in OOP we're constantly changing the same variable (e.g.,
this.state
) in a very controlled way, but in FP we still need the data to change but instead of actually changing a variable, you replace the old variables (hence the immutability). We never change anything, we simply overwrite what used to be there. We have a super awesome example that we have been using, and it is called REDUX. Redux uses FP because there is state (i.e., we do have a store), but what informs the store to decide what is in the store? Well, we have the reducers. Those are what decide what's inside of the store. We have a bunch of little reducers. What do the reducers return? They never ever change a piece of state. They simply return something different. So they never mutate state. They simply update the store with a totally different new variable. This isn't to say we don't change the store. We do. We do it all the time. But we don't mutate the stuff inside of the store. We overwrite what used to be there, and we do this with our reducers which are pure functions.
As can be seen from the above descriptions of everything from OOP and FP, purity and immutability are very different from the mutation that goes on in OOP. This should give you more context for both paradigms and the merits of both.
Paradigm chart
Consider the following programming paradigm chart we will use to guide us through the next few notes:
We are now going to continue working through what we started to address in the previous note only in a more "these are the actual ways to program" manner. So before we talked specifically about OOP and FP, but those two aren't actually in competition with each other. They tend to be somewhat these days because programmers from both sides can hate those of the other side. But really these paradigms aren't in competition with each other.
The chart above has a horizontal access that represents how to do state management and a vertical axis that represents how data and methods are associated in an application. These are the ways we will discover how we want to organize our code and what paradigm we want to use. There are a lot of different ways to look at this particular concept but this is how we will frame things for now and we will subsequently explore and code out each thing listed above.
The first option for state management is the imperative programming paradigm. In regards to state management, imperative programming means that you simply mutate state. If you want to change the value of a variable, then you change it. There is no shared state. You make a variable and then you change it whenever you need to. This is almost certainly how you learned to program because it's awesome when you have a small problem. You simply get work done and can start coding right away. You don't have to worry about setting up really complicated structures.
The second option for state management is the functional programming paradigm. In regards to state management, functional programming means we primarily have shared state.
If we now go to how the data and methods are associated with each other, we will have OOP and procedural.
In procedural programming, that just means there is no association between the data and the methods.
- Imperative and Procedural (I + P): Just like with imperative, we simply mutate state. It's kind of like the wild wild west. Procedural is the same way. If you need a function, then you write it. It doesn't make any difference what data it uses. You can use any data you want and you can declare data anywhere you want. A lot of times this just means you're using a ton of globals, and this is probably where you started programming (i.e., imperative and procedural).
- Functional and Procedural (F + P): This means you are going to manage state very carefully. You will avoid mutating it whenever possible. And the data you are passing around doesn't have to be associated with a particular collection of functions because all of the functions are pure.
- Imperative and Object-Oriented (I + OOP): This means we are going to mutate state, but we are going to handle the state in the small, controlled, and encapsulated little objects. We have our little objects to keep all the setters and getters in one place, and we're just going to mutate directly but in a very controlled way.
- Functional and Object-Oriented (F + OOP): FP and OOP, as this point illustrates, can work together. That would mean we would try to eliminate state if at all possible, but any time where we have to use state, we'll put it inside of objects.
Hooks
Back to Hooks
If we head back to the docs and look at the Hooks at a Glance section, then we will see material on the State Hook and the Effect Hook. These will be the Hooks you use most often and the React docs has a section on the useState Hook as well as a section on the useEffect Hook.
So there are two main Hooks we will use (there are others and we can write some ourselves but these will be the most common): State Hook, Effect Hook. These are the Hook names, and we use them like so:
import React, { useState, useEffect } from 'react';
That is, to use the State Hook and the Effect Hook, you import useState
and useEffect
, respectively. Generally, "use" prepends the name of the Hook you will use (e.g., useContext
, useReducer
, etc.). Here is the Hooks API reference, and we can see the Hooks categorized as Basic and Additional:
The useState
hook, like we saw before, gives us the ability to interact with or use component-level state just like we always have with our class components but inside of a function component. The useEffect
hook allows us to use component lifecycle methods. So things like componentDidMount
, componentDidUpdate
, componentWillUmount
, etc., all require us to use the useEffect
hook. This is because these methods come as a result of extending React.Component
in our class methods. So we need some way to tap into them inside of functional components and the way we do that is with the useEffect
hook.
Let's now revisit our super basic counter component that used useState
:
import React, {useState} from 'react';
import './App.css';
function App() {
const [counter, setCounter] = useState(0);
return(
<div>
<div>Counter: {counter}</div>
<button onClick={() => setCounter(counter + 1)}>Add One!</button>
</div>
)
}
The line
const [counter, setCounter] = useState(0);
may look somewhat intimidating but it shouldn't be. It's array destructuring and very similar to what have done before with something like
const { name, age } = this.state;
where we create variables name
and age
from this.state.name
and this.state.age
, respectively. We're doing the exact same thing with const [counter, setCounter] = useState(0);
except when you are talking about arrays you are going to grab things by index. Clearly, then, somehow useState(0)
must be returning an array. If we drop a little console.log(useState(0))
below const [counter, setCounter] = useState(0);
then we can see what this is exactly:
So we can see that calling useState(0)
returns an array of two elements where the first element is the number 0
, and the second element is a function. We don't need to worry about all the function details right now, but just know React is giving you that. So what just happened? What do the two elements coming from useState
do exactly?
Well, whatever we pass to useState
is going to be the first element in the array. So when we do
const [counter, setCounter] = useState(0);
what we are effectively doing is initializing counter
to be 0
.
So whatever we pass the useState
Hook is going to be our initial piece of state; in the example above, the piece of state counter
will have an initial value of 0
. Now, setCounter
, on the other hand, is going to be the means by which we interact with our piece of state. So setCounter
is going to be kind of like calling this.setState
.
A couple notes from what we have just discussed:
- The zeroth index of
useState(arg)
is effectivelythis.state.arg
in the lens of what we have used previously. - The first index of
useState(arg)
is effectivelythis.setState({ arg })
Said more concisely and differently:
[0]
orcounter
= thethis.state
[1]
orsetCounter
=this.setState(thing [0])
And note that the 0
we passed useState(0)
previously when we ran const [counter, setCounter] = useState(0);
is effectively equivalent to initializing this.state = { counter: 0 }
in a class.
It's worth noting that Hooks do not use an object to store state like we have done previously with classes. We're not putting things in objects where we have things like this.state.WHATEVER
. From now on, inside of the Hooks landscape, we are just using a plain variable. So counter
contains the value of the state for our counter, and it is not associated with any other piece of state. And it is also not stored inside of an object that we keep track of. It's just its own variable. And really this is just some "frosting"; that is, React state works the same in Hooks as it does when we are using classes. You are not getting any new power--you're just interacting with the React API differently than we have in the past. Exactly the same stuff, same performance, same security, etc. It's just: How do you want to interact with React? Do you want to use the Hooks system and just write functions? Or do you want to use classes? Either way React is going to do the same thing under the hood.
The return of the weather widget!
We are now going to tackle making HTTP requests with Hooks because this is one of the most common things you will do with React (you're going to constantly need data from somewhere). And the Hooks system is rather nice in this arena.
TBD ...
Supplemental Notes
Prop Types
Prop types (typechecking with propTypes
)
See the docs for all the gory details. Most often we will want to ensure prop values are of a certain type (e.g., string, number, etc.) with static propTypes = { ... }
or as outlined in the docs. We will also occasionally want to supply default values for props using static defaultProps = { ... }
.
Default prop values
Occasionally, we will want to give default values to certain props for a component (as opposed to using messy destructuring with default values). This note outlines how to do that.
As noted in the docs, you define default values for your props by assigning to the special defaultProps
property:
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
// Specifies the default values for props:
Greeting.defaultProps = {
name: 'Stranger'
};
// Renders "Hello, Stranger":
ReactDOM.render(
<Greeting />,
document.getElementById('example')
);
As the docs also note: If you are using a Babel transform like transform-class-properties (which we usually will be), you can also declare defaultProps
as static property within a React component class. This syntax has not yet been finalized though and will require a compilation step to work within a browser. For more information, see the class fields proposal.
class Greeting extends React.Component {
static defaultProps = {
name: 'stranger'
}
render() {
return (
<div>Hello, {this.props.name}</div>
)
}
}
The defaultProps
will be used to ensure that this.props.name
will have a value if it was not specified by the parent component. The propTypes
typechecking happens after defaultProps
are resolved, so typechecking will also apply to the defaultProps
.
Converting class component to functional component but keeping default props and prop types
Suppose we wanted to convert the following class component to a functional component but wanted to keep the defaultProps
as well as the propTypes
:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class NavBar extends Component {
static defaultProps = {
title: 'GitHub Finder',
icon: ['fab', 'github']
}
static propTypes = {
title: PropTypes.string.isRequired,
icon: PropTypes.array.isRequired
}
render() {
const {title: navBarTitle, icon} = this.props;
return (
<nav className="navbar bg-primary">
<h1><FontAwesomeIcon icon={icon} /> {navBarTitle}</h1>
</nav>
);
}
}
export default NavBar;
Since we have a class, we can use static defaultProps/propTypes
, but we cannot use the static
keyword without class. The solution is to simply follow the example in the docs like so:
import React from 'react'
import PropTypes from 'prop-types'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
const NavBar = (props) => {
const {title: navBarTitle, icon} = props;
return (
<nav className="navbar bg-primary">
<h1><FontAwesomeIcon icon={icon} /> {navBarTitle}</h1>
</nav>
);
}
NavBar.defaultProps = {
title: 'GitHub Finder',
icon: ['fab', 'github']
}
NavBar.propTypes = {
title: PropTypes.string.isRequired,
icon: PropTypes.array.isRequired
}
export default NavBar;
Hence, we simply have ComponentName.propsInvolvedThing = { ... }
outside of the functional component. We can do the same thing in classes if we wanted to as also illustrated in the docs.
Hooks
useEffect
(basic usage, similarity to class lifecycle methods, peculiarities, etc.)
The docs are fairly extensive on the useEffect
Hook, but we can cover some of the essentials here. The first "tip" we encounter is the following from the React team: "If you're familiar with React class lifecycle methods, you can think of useEffect
Hook as componentDidMount
, componentDidUpdate
, and componentWillUnmount
combined." To get a better sense of what this really means, this Medium article points out the following very helpful guide (slightly modified below):
// (1) On mount and every render
useEffect (() => {
dosomething()
});
// (2) Only on mount (similar to componentDidMount)
useEffect (() => {
dosomething()
}, []);
// (3) On mount/every time state of count changes (similar to componentDidUpdate)
useEffect (() => {
dosomething()
}, [count]);
// (4) UseEffect with cleanup (similar to componentWillUnmount)
useEffect (() => {
dosomething();
return clearSomething(){};
});
As we can see from the above, if we are going to do something like issue AJAX requests, which would typically go inside of componentDidMount
in a class, then we will do this inside of the following:
useEffect (() => {
dosomething() // e.g., AJAX request(s)
}, []);
What happens if we do not include the empty array? For example:
useEffect (() => {
dosomething() // e.g., AJAX request(s)
});
This will result in making infinitely many requests (i.e., we get stuck in an infinite loop). Why? Well, assuming our AJAX requests are tied to pieces of state (which they inevitably are), then we just changed state which means we get a render. Since no empty array is specified, useEffect
will fire off again, resulting in another state change (even if the data is the same), which results in another render, etc. Specifying the empty array is similar to putting our AJAX request(s) into componentDidMount
, and this accomplishes the desired behavior. Unfortunately, with create-react-app
, not specifying anything in the array can result in a misleading error from the linter:
If we follow the advice in some instances (e.g., when making AJAX calls), then we actually end up with the same looping problem. This is similar to the problem you can run into when trying to set state inside of componentDidUpdate
in a class. As the docs note, "In some cases, cleaning up or applying the effect after every render might create a performance problem. In class components, we can solve this by writing an extra comparison with prevProps
or prevState
inside componentDidUpdate
":
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
They go on: "This requirement is common enough that it is built into the useEffect
Hook API. You can tell React to skip applying an effect if certain values haven't changed between re-renders. To do so, pass an array as an optional second argument to useEffect
":
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
The problem with doing this with AJAX calls oftentimes is that the data you are using to set state will actually be different on each call, effectively resulting in rerendering infinitely often. We will often want to just treat our initial calls as we might in componentDidMount
, using only []
in useEffect
. But the eslint error can get really annoying to look at in your console when it's really not an error at all. The low-grade solution (at least for now...a lot of people aren't happy with this fake error) is the following:
useEffect (() => {
dosomething() // e.g., AJAX request(s)
// eslint-disable-next-line
}, []);
This effectively disables the linter for the next line which causes the warning. Not a great fix but it will have to do for now.
Context
Remarks on Hooks and Context API
Hooks allow us to basically use functional components which are usually slimmer and cleaner than class components. So what exactly are Hooks? They are functions that let us hook into React state and component lifecycle features from a function component rather than a class component. Some of the hooks you will see:
- useState
- useEffect (this lets you mimic lifecycle methods of classes and this Medium article has some nice details on how this is accomplished)
- useContext
- useReducer
- The above hooks are some of the more basic hooks React gives us access to via something like, for example,
import { useState, useEffect } from 'react';
. Here is the Hooks API reference where other hooks such asuseReducer
,useCallback
,useMemo
,useRef
, etc., can be found. Basically, theuseState
hook gives us the ability to use state within functional components whereas before with classes we had to usestate = { ... }
orthis.state = { ... }
to have access to state. TheuseEffect
hook gives us the ability to mimic class lifecycle methods but within functions. TheuseContext
anduseReducer
hooks are largely used within the confines of the Context API.
Speaking of the Context API, it does not take long to realize that passing props multiple levels down can get arduous and messy very quickly. This is where something like the Context API can be of use (or Redux). As the React docs say, "Context provides a way to pass data through the component tree without having to pass props down manually at every level. In a typical React application, data is passed top-down (parent to child) via props, but this can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree."
Essentially, when state is only used at the component level, then using state
or this.state
in a class will do or the useState
hook in a functional component will do. But if we want our state to possibly be shared by numerous components, then it is time to look at having application-level state and using a robust state management tool like the Context API or Redux.
Informally, the useContext
hook makes it possible to very easily bring in your context into any component whereas before there were hooks this was quite difficult and ugly. The useReducer
hook allows us to create a really nice Redux-like reducer without having to using something as full-blown as Redux. And a reducer is a function that you can basically dispatch actions to in order to manipulate your application-level state where that state will then be sent down to the different components that subscribe to that piece of state.
Here is a diagram to attempt to sort of explain how the Context API works:
The important thing to keep in mind here is that there is not really a standardized way of going about implementing the Context API (at least not right now). The image references how Brad Traversy uses the Context API.
The Context API allows us to remove what you might otherwise stuff in App.js
in order to make available to a bunch of other components via prop-drilling. And you can have multiple contexts. The larger your application is and the more resources you have, the more contexts you will likely have. Brad mentions having a folder for each context where each folder will have three files, something like the following:
contexts
|- GitHub
|--- githubContext.js (initialize Context API here; takes the form: resourceContext.js)
|--- GitHubState.js (GitHub context global state; action creators; exported provider to wrap App so we can easily access state as well as action creators from any component in App without prop-drilling)
|--- githubReducer.js (decides what state looks like after each action creator fires an action)
Context vs Redux
Without getting too embroiled in the "controversy", Brad Traversy seems to think Redux is great for very very large applications while the Context API is great for small- to medium-sized applications where you still want to have application-level state but with not quite as much overhead as fully implementing Redux might require.