An Introduction to npm
If you think about the underlying architecture of writing software, the program is usually comprised of a collection of modules (a module being some code that is grouped together, usually by file). If you have one or more modules that are program agnostic, meaning they can be reused in other programs, you’d create a “package”.
Program
App.js <- Module
Dashboard.js <- Module
About.js <- Module
Math <- Package
add.js <- Module
subtract.js <- Module
multiply.js <- Module
divide.js <- Module
This package architecture is what makes the JavaScript ecosystem so powerful. If there’s a package you need, odds are it’s already been created and is available to download for free. Want to use Lodash? You’d download the lodash
package. Want to use MomentJS to better manage timestamps in your app? Download the moment
package. What about React? Yup, there’s a react
package. Now the question becomes, how and from where do we download these packages?
CDNs and script tags
The traditional way is to create a <script>
tag that links to a CDN where the package is hosted or if you download it locally, the path to that file.
<body>
...
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="libs/react.min.js"></script>
</body>
This approach works, but it doesn’t scale very well. First, if the CDN servers crash, your app crashes with it. Second, the order of the <script>
tags matters. If library B is dependent on Library A, but the <script>
tag to load library B comes before the <script>
to load library A, things will break. Finally, you have a versioning problem. If jQuery releases a new version, you either need to manually swap out the CDN (assuming there’s an updated one), or you’ll need to re-download the new version to have it locally. Now for just one package this probably isn’t a big deal, but as your application grows and you start having to manually manage 20+ packages, it’s going to be a pain.
So let’s try to conjure up a solution; here’s a list of what we need.
- Make it easier to download packages
- Make it easier to upload packages for others to consume
- Make it easier to switch versions of our packages
- Do it all for free
Luckily for us, there’s a company which solved all of our problems.
npm, Inc.
npm, Inc. is a for-profit, venture-backed company founded in 2014 and was acquired by Github in March of 2020. They host and maintain “npm” (short for Node.js package manager). npm consists of two parts: a registry (for hosting the packages) and a CLI (for accessing and uploading packages). At the time of this writing, the npm registry has over 800,000 packages being installed over 2 billion times a day by over 11 million JavaScript developers, .
Installing npm
In order to use the npm CLI, you’ll need to install it. However, if you already have Node installed, you should already have npm as it comes with Node. If you don’t have Node installed, you can download it from here or use a tool like Homebrew.
If Node and npm are installed correctly, you should be able to run the following commands in your terminal to check which versions you have installed.
node -v # My installed version: v11.10.0
npm -v # My installed version: 6.9.0
npm init
Now that you have Node and npm installed, the next step is to actually download a package. Before you do that, though, you’ll want to initialize your new project with npm. You can do that by running npm init
inside of your project’s directory. This will walk you through some steps for initializing your project. Once finished, you’ll notice you have a brand new package.json
file and an empty node_modules
directory.
node_modules
Whenever you install a package, the source code for that package will be put inside of the node_modules
directory. Then, whenever you import a module into your project that isn’t a file path, i.e. import React from 'react'
, your app will look to node_modules
for the source.
package.json
You can think of your package.json
file as containing all of the meta information for your project. It contains information like the project’s name, author, description, and most important, the list of packages (as well as what versions) that your project depends on as well as how to run your project - here’s an example.
{
"name": "github-battle",
"version": "1.0.0",
"description": "Compare two Github user's profile.",
"author": "Tyler McGinnis",
"license": "ISC",
"homepage": "https://github.com/tylermcginnis/react-course#readme",
"keywords": [
"react",
"react-router",
"babel",
"webpack"
],
"repository": {
"type": "git",
"url": "git+https://github.com/tylermcginnis/react-course.git"
},
"main": "index.js",
"dependencies": {
"prop-types": "^15.7.2",
"query-string": "^6.2.0",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-icons": "^3.4.0",
"react-router-dom": "^4.3.1"
},
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/plugin-proposal-class-properties": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.5",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"copy-webpack-plugin": "^5.0.0",
"css-loader": "^2.1.0",
"html-webpack-plugin": "^3.2.0",
"style-loader": "^0.23.1",
"webpack": "^4.29.5",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1"
},
"scripts": {
"start": "webpack-dev-server --open",
"build": "NODE_ENV='production' webpack",
}
}
A few properties to point out.
dependencies
These are the packages your application needs to run. Whenever you install a new package, the source for that package will be placed into the node_modules
directory and the name and version of that package will be added to the dependencies
property in your package.json
file.
devDependencies
If dependencies
are the packages your application needs to run, devDependencies
are the packages your application needs during development.
npm install
The reason it’s so important to keep track of your dependencies
and devDependencies
is if someone downloads your project and runs npm install
, npm will download all of the packages inside both dependencies
and devDependencies
and place them into the node_modules
directory. This makes it so when you push your code to Github, instead of having to push up your entire node_modules
directory, you can instead keep track of your dependencies and install them when needed using npm install
.
The reason dependencies
are separate from devDependencies
is so you can build your app for production. In production, you don’t care about the packages needed to develop your app; you only care about the packages needed to run your app.
scripts
You can use the scripts
property to automate tasks. In the example above, we have two, start
and build
.
In order to run your script, cd
into the same directory as the package.json
file and from the command line run, npm run [NAME OF SCRIPT]
. In our example, we have our start
script running webpack-dev-server --open
. In order to execute that script, from the command line we’d run npm run start
.
Installing Packages
Now that we know all about initializing our project with npm init
, node_modules
, and the package.json
file, the next step is to learn how to actually install a package from the npm registry. To do this, from the command line, run npm install package-name
.
npm install react
That command will do a few things. It’ll put the react
package inside of our node_modules
directory as well as add react
as a property on our dependencies
object inside our package.json
file.
To tell npm you’re installing a developer dependency (and it should be put it in devDependencies
instead of dependencies
), you’ll append the --save-dev
flag.
npm install webpack --save-dev
Publishing Packages
There wouldn’t be over 800,000 packages on the npm registry if it wasn’t easy to publish one. All you need to publish a package is an account on npm, a package.json file with name
, version
, and main
(which points to the entry point of your package) properties.
Once you have those things, in your command line run npm login
to login then npm publish
to publish your package.
There are more advanced features about publishing that we won’t go into in this post, but if you’re curious, you can check out their official guide.
Versioning
Earlier one of our needs was the ability to more efficiently manage the different versions of the packages we were using. The npm CLI helps us out here as well.
Typically each package on the npm registry follows semantic versioning. There are three parts to semantic versioning, major versions, minor versions, and patch versions.
v1.2.3
In the version above, the major version is 1
, the minor version is 2
, and the patch version is 3
.
The idea is if you’re a library author and you had a breaking change, you’d increment the major version. If you had a new, non-breaking feature, you’d increment the minor version. For everything else, you’d increment the patch version.
So why is this important? We want to avoid having our app break because we installed the wrong version of a package. npm gives us some tools to prevent this.
^
If you look at the dependencies
inside of our package.json
file again, you’ll notice that before each version number, there’s a little ^
.
"dependencies": {
"prop-types": "^15.7.2",
"query-string": "^6.2.0",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-icons": "^3.4.0",
"react-router-dom": "^4.3.1"
}
What the ^
does is it instructs npm to install the newest version of the package with the same major version. So for example, if the prop-types
package released v15.8.0, when we ran npm install
on our project, we’d get that new version. However, if there was a breaking change and prop-types
released v16.0.0, only the newest v15.X.X version would be installed and not the breaking v16.0.0 version.
~
If instead, you wanted to have both the major and minor version match, you’d use ~
.
"dependencies": {
"prop-types": "~15.7.2"
}
Now, if v16.0.0
or v15.8.0
came out, neither would be installed. However, if v15.7.3
came out, it would be installed since its the newest version where both the major and minor versions match.
Exact version
Finally, if you wanted to only download the exact version of what’s listed in your package.json
file, you’d list only the version number.
"dependencies": {
"prop-types": "15.7.2"
}
Now, only v15.7.2 will ever be installed.
If you want to see a few less common options for specifying acceptable ranges, you can check out the Semver Calculator
Webpack: A Gentle Introduction
Whenever you’re learning a new tool, you should first ask yourself two questions.
- Why does this tool exist?
- What problems does this tool solve?
If you can’t answer both of those questions, you may not need the tool in the first place. Let’s take those questions and apply them to webpack.
Why does webpack Exist?
At its core, webpack is a module bundler. It examines all of the modules in your application, creates a dependency graph, then intelligently puts all of them together into one or more bundle(s) that your index.html
file can reference.
App.js ---> | |
Dashboard.js -> | Bundler | -> bundle.js
About.js ---> | |
What problem is webpack solving?
Historically when building a JavaScript application, your JavaScript code would be separated by files (these files may or may not have been actual modules). Then in your index.html
file, you’d have to include <script>
tags to every JavaScript file you had.
<body>
...
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="libs/react.min.js"></script>
<script src='src/admin.js'></script>
<script src='src/dashboard.js'></script>
<script src='src/api.js'></script>
<script src='src/auth.js'></script>
<script src='src/rickastley.js'></script>
</body>
Not only was this tedious, but it was also error-prone. There were the obvious issues like typos or forgetting to include a file, but more than that, the order of the <script>
tags mattered. If you loaded a script that depended on React before loading the React script, things would break. Because webpack (intelligently) creates a bundle for you, both of those problems go away. You don’t have to worry about forgetting a <script>
and you don’t have to worry about the order.
<body>
...
<script src='dist/bundle.js'></script>
</body>
As we’ll soon see, the “module bundling” aspect is just one part of webpack. If needed, you’re also able to tell webpack to make certain transformations on your modules before adding them to the bundle. Examples might include transforming SASS/LESS to regular CSS or “modern JavaScript” to ES5 that the browser can understand.
Installing webpack
Assuming you’ve initialized a new project with npm, there are two packages you need to install to use webpack, webpack
and webpack-cli
.
npm install webpack webpack-cli --save-dev
webpack.config.js
Once you’ve installed webpack
and webpack-cli
, it’s time to start configuring webpack. To do that, you’ll create a webpack.config.js
file that exports an object. Naturally, this object is where all the configuration settings for webpack will go.
// webpack.config.js
module.exports = {}
Remember, the whole point of webpack is to “examine all of your modules, (optionally) transform them, then intelligently put all of them together into one or more bundle(s)” If you think about that process, in order to do that, webpack needs to know three things.
- The entry point of your application
- Which transformations, if any, to make on your code
- The location to put the newly formed bundle(s)
The entry point
Whenever your application is composed of modules, there’s always a single module that is the entry point of your application. It’s the module that kicks everything off. Typically, it’s an index.js
file. Something like this.
index.js
imports about.js
imports dashboard.js
imports graph.js
imports auth.js
imports api.js
If we give webpack the path to this entry file, it’ll use that to create the dependency graph of our application (much like we did above, except… better). To do that, you add an entry
property to your webpack config which points to your entry file.
// webpack.config.js
module.exports = {
entry: './app/index.js'
}
Transformations with Loaders
Now that webpack knows the entry file, the next thing we need to tell it is what transformations to run on our code. To do this, we’ll use what are called “loaders”.
Out of the box, when webpack is building its dependency graph by examining all of your import
/ require()
statements, it’s only able to process JavaScript and JSON files.
import auth from './api/auth' // 👍
import config from './utils/config.json' // 👍
import './styles.css' // ⁉️
import logo from './assets/logo.svg' // ⁉️
There’s a very good chance that you’re going to want your dependency tree to be made up of more than just JS and JSON files - i.e., you’re going to want to be able to import .css
files, .svg
files, images, etc, as we’re doing above. This is where “loaders” can help us out. The primary purpose of a loader, as the name suggests, is to give webpack the ability to process more than just JavaScript and JSON files.
The first step to adding any loader is to download it. Because we want to add the ability to import
.svg
files in our app, we’ll download the svg-inline-loader
from npm.
npm install svg-inline-loader --save-dev
Next, we need to add it to our webpack config. All of the information for your loaders will go into an array of objects under module.rules
.
// webpack.config.js
module.exports = {
entry: './app/index.js',
module: {
rules: []
}
}
Now there are two pieces of information we need to give webpack about each loader. First, the type of file we want to run the loader on (in our case, all .svg
files). Second, the loader to use on that file type (in our case, svg-inline-loader
).
To do this, we’ll have an object with two properties, test
and use
. test
will be a regex to match the file path and use
will be the name of the loader we want to use.
// webpack.config.js
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.svg$/, use: 'svg-inline-loader' }
]
}
}
Now anywhere in our app, we’ll be able to import .svg
files. What about our .css
files though? Let’s add a loader for that as well. We’ll use the css-loader
.
npm install css-loader --save-dev
// webpack.config.js
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.svg$/, use: 'svg-inline-loader' },
{ test: /\.css$/, use: 'css-loader' }
]
}
}
Now anywhere in our app, we can import .svg
and .css
files. However, there’s still one more loader we need to add to get our styles to work properly. Right now, because of our css-loader
, we’re able to import
.css
files. However, that doesn’t mean those styles are being injected into the DOM. What we really want to do is import
a CSS file then have webpack put all of that CSS in a <style>
tag in the DOM so they’re active on the page. To do that, we’ll use the style-loader
.
npm install style-loader --save-dev
// webpack.config.js
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.svg$/, use: 'svg-inline-loader' },
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }
]
}
}
Notice, because we now have two loaders for our .css
rule, we change use
to be an array. Also, notice that we have style-loader
before css-loader
. This is important. Webpack will process those in reverse order. So css-loader
will interpret the import './styles.css'
line then style-loader
will inject that CSS into the DOM.
As we just saw with style-loader
, loaders can do more than just allow you to import
certain file types. They’re also able to run transformations on files before they get added to the final output bundle. The most popular is transforming “next generation JavaScript” to the JavaScript of today that browsers can understand using Babel. To do this, you can use the babel-loader
on every .js
file.
npm install babel-loader --save-dev
// webpack.config.js
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.svg$/, use: 'svg-inline-loader' },
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
{ test: /\.(js)$/, use: 'babel-loader' }
]
}
}
There are loaders for just about anything you’d need to do. You can check out the full list here.
The output
Now that webpack knows the entry file and what loaders to use, the next thing we need to tell it is where to put the bundle it creates. To do this, you add an output
property to your webpack config.
// webpack.config.js
const path = require('path')
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.svg$/, use: 'svg-inline-loader' },
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
{ test: /\.(js)$/, use: 'babel-loader' }
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index_bundle.js'
}
}
So the full process looks something like this.
- webpack grabs the entry point located at
./app/index.js
. - It examines all of our
import
andrequire
statements and creates a dependency graph. - webpack starts creating a bundle, whenever it comes across a path we have a loader for, it transforms the code according to that loader then adds it to the bundle.
- It takes the final bundle and outputs it at
dist/index_bundle.js
.
Plugins
We’ve seen how you can use loaders to work on individual files before or while the bundle is being generated. Unlike loaders, plugins allow you to execute certain tasks after the bundle has been created. Because of this, these tasks can be on the bundle itself, or just to your codebase. You can think of plugins as a more powerful, less restrictive version of loaders.
Let’s take a look at a few examples.
HtmlWebpackPlugin
Earlier we saw that the main benefit of webpack was that it would generate a single bundle for us that we could then use to reference inside of our main index.html
page.
What HtmlWebpackPlugin
does is it will generate this index.html
page for us, stick it inside of the same directory where our bundle is put, and automatically include a <script>
tag which references the newly generated bundle.
So in our example, because we’ve told webpack to name the final bundle index_bundle.js
and put it in a folder called dist
, when HtmlWebpackPlugin
runs, it’ll create a new index.html
file, put it in dist
, and include a script to reference the bundle, <script src='bundle_index.js'></script>
. Pretty nice, right? Because this file is being generated for us by HtmlWebpackPlugin
, even if we change the output path or file name of our bundle, HtmlWebpackPlugin
will have that information and it’ll adapt accordingly.
Now, how we do adjust our webpack config in order to utilize HtmlWebpackPlugin
? As always, we first need to download it.
npm install html-webpack-plugin --save-dev
Next, we add a plugins
property which is an array to our webpack config.
// webpack.config.js
const path = require('path')
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.svg$/, use: 'svg-inline-loader' },
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
{ test: /\.(js)$/, use: 'babel-loader' }
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index_bundle.js'
},
plugins: []
}
Then in order to use HtmlWebpackPlugin
, we create a new instance of it inside of our plugins
array.
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.svg$/, use: 'svg-inline-loader' },
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
{ test: /\.(js)$/, use: 'babel-loader' }
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index_bundle.js'
},
plugins: [
new HtmlWebpackPlugin()
]
}
EnvironmentPlugin
If you’re using React, you’ll want to set process.env.NODE_ENV
to production
before you deploy your code. This tells React to build in production mode which will strip out any developer features like warnings. Webpack makes this simple by providing a plugin called EnvironmentPlugin
. It comes as part of the webpack
namespace so you don’t need to download it.
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.svg$/, use: 'svg-inline-loader' },
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
{ test: /\.(js)$/, use: 'babel-loader' }
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index_bundle.js'
},
plugins: [
new HtmlWebpackPlugin(),
new webpack.EnvironmentPlugin({
'NODE_ENV': 'production'
})
]
}
Now, anywhere in our application, we’ll be able to tell if we’re running in production mode by using process.env.NODE_ENV
.
HtmlWebpackPlugin
and EnvironmentPlugin
are just a small taste of what you can do with webpack’s plugin system. Here’s a full list of officially supported plugins.
Mode
Whenever you build your app for production, there are a few steps you want to take. We just learned about one of them which was setting process.env.NODE_ENV
to production
. Another would be minifying your code and stripping out comments to reduce the bundle size.
Utilizing plugins for each one of these production tasks would work, but there’s a much easier way. In your webpack config, you can set the mode
property to development
or production
depending on which environment you’re in.
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.svg$/, use: 'svg-inline-loader' },
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
{ test: /\.(js)$/, use: 'babel-loader' }
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index_bundle.js'
},
plugins: [
new HtmlWebpackPlugin()
],
mode: 'production'
}
Notice we were able to get rid of our EnvironmentPlugin
. The reason for that is by setting mode
to production
, webpack will automatically set process.env.NODE_ENV
to production
. It will also minify our code and strip out warnings.
Running webpack
At this point, we have a pretty solid grasp on how webpack works and how to configure it, the only other thing we need to do now is actually run it.
Assuming you’re using npm and have a package.json
file, you can create a script
to execute webpack
.
// package.json
"scripts": {
"build": "webpack"
}
Now whenever you run npm run build
from the command line, webpack
will execute and create an optimized bundle named index_bundle.js
and put it inside of the dist
directory.
Production vs Development Modes
At this point, there’s nothing more about webpack itself that we’re going to cover. However, it is important that you understand how to easily switch between running in development
mode and running in production
mode.
As we talked about, when we’re building for production
, we want everything to be as optimized as possible. When we’re building for development
, the opposite is true.
To make it easy to switch between production
and development
builds, we’ll have two different commands we can run via our npm scripts
.
npm run build
will build our app for production.
npm run start
will start a development server which will automatically regenerate our bundle whenever we make a change to our code.
If you’ll remember, we hardcoded mode
to production
inside of our webpack config. However, we only want to run in production
mode when we run npm run build
. If we run npm run start
, we want mode
set to development
. To fix this, let’s adjust our scripts.build
property in our package.json
file to pass along an environment variable.
"scripts": {
"build": "NODE_ENV='production' webpack",
}
If you’re on Windows, the command is a bit different:
"SET NODE_ENV='production' && webpack"
Now, inside of our webpack config, we can toggle mode
based on process.env.NODE_ENV
.
// webpack.config.js
...
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'
}
Now whenever we want to build our app for production, we just run npm run build
in our command line. That will generate an index.html
file and an index_bundle.js
file and put them in the dist
directory.
webpack DevServer
Unlike building for production, when we’re developing, it’s all about speed. We don’t want to have to re-run webpack
and wait for it to rebuild the dist
directory every time we change our code. This is where the webpack-dev-server
package can help us out.
As the name implies, webpack-dev-server
is a development server for webpack. Instead of generating a dist
directory, it’ll keep track of your files in memory and serve them via a local server. More than that, it supports live reloading. What that means is whenever you make a change in your code, webpack-dev-server
will quickly recompile your code and reload the browser with those changes.
As always, to use it we first need to install it.
npm install webpack-dev-server --save-dev
Then all we need to do is update our start
script to run webpack-dev-server
.
"scripts": {
"build": "NODE_ENV='production' webpack",
"start": "webpack-dev-server"
}
Just like that, we have two commands, one for creating a development server and one for building our app for production.
(Project) First Component
The code for this video can be found here.
The commit for this video can be found here.
npm install --save-dev @babel/core @babel/preset-env @babel/preset-react webpack webpack-cli webpack-dev-server babel-loader css-loader style-loader html-webpack-plugin
JSX Tips and Gotchas for Beginners
For the most part, JSX should feel pretty natural. There are a few things to be aware of though.
Variables in JSX
Whenever you want to use an expression (something that produces a value) in JSX, you need to wrap the expression in single curly braces, {}
.
render() {
const name = 'Tyler'
return (
<div>
<h1>Hello, {name}</h1>
<p>Today is {new Date().toLocaleDateString()}</p>
<p>What is 2 + 2? {2 + 2}</p>
</div>
)
}
Rendering nothing
If you want React to render nothing, return null
.
render() {
if (isLoading() === true) {
return null
}
return (
...
)
}
Conditional Rendering
The ability to conditionally render UI based on a piece of state is pretty foundational to any front-end framework. Typically, this functionality is built natively into the framework.
// Angular
<h1 *ngIf="authed; else elseBlock">Welcome back!</h1>
<ng-template #elseBlock><h1>Login to see your dashboard</h1></ng-template>
// Vue
<h1 v-if="authed">Welcome back!</h1>
<h1 v-else>Login to see your dashboard</h1>
With React, it’s a bit different. Instead of increasing the API surface layer, because JSX is “Just JavaScript”, React can leverage native JavaScript features to accomplish the same task. There are pros and cons to this approach, but if you’re already familiar with conditional rendering in JavaScript, you’ll feel pretty comfortable.
If/else
The most basic example is just using a simple if/else statement.
render() {
const authed = isAuthed()
if (authed === true) {
return <h1>Welcome back!</h1>
} else {
return <h1>Login to see your dashboard</h1>
}
}
Again, because we’re just writing JavaScript, if we had another conditional, we’d just add an else if
case.
render() {
const authed = isAuthed()
const firstLogin = isNew()
if (firstLogin === true) {
return <h1>👋 Welcome!</hi>
} else if (authed === true) {
return <h1>Welcome back!</h1>
} else {
return <h1>Login to see your dashboard</h1>
}
}
Ternary Operator
If you’re rendering different UI based on a single condition, typically you’d use JavaScript’s ternary operator.
render() {
return isAuthed() === true
? <h1>Welcome back!</h1>
: <h1>Login to see your dashboard</h1>
}
We learned earlier that any expression needs to be wrapped in {}
. We can use that knowledge to render a ternary inside of JSX.
render() {
return (
<div>
<Logo />
{isAuthed() === true
? <h1>Welcome back!</h1>
: <h1>Login to see your dashboard</h1>}
</div>
)
}
Earlier we also learned that we can render null
if we want React to render nothing. This is a common pattern when using ternaries.
render() {
return (
<div>
<Logo />
{showWarning() === true
? <Warning />
: null}
</div>
)
}
Logical && Operator
If you’re not already familiar with it, JavaScript has an &&
operator. Typically it’s used in conditionals as an “AND” statement.
if (user && authed) {}
In the example above, it’s important to note that authed
won’t be checked if user
isn’t truthy. Using that logic, we can use the &&
operator as a more concise ternary that renders null.
render() {
return (
<div>
<Logo />
{showWarning() === true && <Warning />}
</div>
)
}
React Fragments
Can you spot what’s wrong with the following JSX code?
render() {
const name = 'Tyler'
return (
<h1>Hello, {name}</h1>
<p>Today is {getDay()}</p>
<p>What is 2 + 2? {2 + 2}</p>
)
}
It looks fine, right? Unfortunately, it’ll throw an error.
Adjacent JSX elements must be wrapped in an enclosing tag.
That’s a fancy way to say that you can only ever return one top-level element from a component. In our example, we’re trying to return 3. We can fix this by wrapping everything in a div
.
render() {
const name = 'Tyler'
return (
<div>
<h1>Hello, {name}</h1>
<p>Today is {getDay()}</p>
<p>What is 2 + 2? {2 + 2}</p>
</div>
)
}
That fixes the issue, but now we have a semantic problem. We’re unnecessarily creating an extra div
. This is the exact use case that React.Fragment
was created for. If you want to return adjacent elements but don’t want to change your markup, wrap them in <React.Fragment>
.
render() {
const name = 'Tyler'
return (
<React.Fragment>
<h1>Hello, {name}</h1>
<p>Today is {getDay()}</p>
<p>What is 2 + 2? {2 + 2}</p>
</React.Fragment>
)
}
Much better.
There also exists a shorthand syntax for React Fragment, but I don’t use it.
render() {
const name = 'Tyler'
return (
<>
<h1>Hello, {name}</h1>
<p>Today is {getDay()}</p>
<p>What is 2 + 2? {2 + 2}</p>
</>
)
}
Capitalization
How does React know the difference between a custom React component like <User />
and a built-in HTML element like <span>
? The answer is probably simpler than you’d expect, it’s based on the capitalization. Whenever you create a React component, you need to capitalize it. Otherwise, React will think it’s a built-in HTML element.
(Bonus) React Elements vs React Components
A few months ago I had, what I thought, was a simple question that I posted to Twitter.
// Function Definition
function add (x, y) {
return x + y
}
// Function Invocation
add(1,2)
// Component Definition
class Icon extends Component
// Component Invocation???
<Icon />
What surprised me wasn’t the joint confusion around this question, but instead was the number of inaccurate responses I received.
Instances / Instantiation
Rendering
Evaluation
Invocation
“Using it :)”
The primary reason for the confusion is that there’s an often un-talked about abstraction layer between JSX and what’s actually going on in React land. To answer this question, we need to take a deep dive into that abstraction.
Let’s start by looking at the absolute fundamentals of React. What is React? It’s a library for building user interfaces. No matter how complex React or the React ecosystem seem to be, this is React at its core — building UIs. With this in mind, we arrive at our first definition, an Element . Simply put, a React element describes what you want to see on the screen . Not so simply put, a React element is an object representation of a DOM node . Notice I used the word describe . It’s important to note that a React element isn’t actually the thing you’ll see on your screen. Instead, it’s just an object representation of it. There are a few reasons for this. The first is that JavaScript objects are lightweight — React can create and destroy these elements without too much overhead. The second reason is React can analyze the object, diff it with the previous object representation to see what changed. Then, React can update the actual DOM only where those changes occurred. This has some performance upsides to it.
In order to create our object representation of a DOM node (aka a React element), we can use React’s createElement
method.
const element = React.createElement(
'div',
{id: 'login-btn'},
'Login'
)
createElement
takes in three arguments. The first is a tag name string ( div
, span
, etc), the second is any attributes you want the element to have, the third is the contents or the children of the element, in this case, the text “Login”. The createElement
invocation above is going to return an object that looks like this.
{
type: 'div',
props: {
children: 'Login',
id: 'login-btn'
}
}
When it’s rendered to the DOM (using ReactDOM.render
), we’ll have a new DOM node that looks like this,
<div id='login-btn'>Login</div>
What’s interesting about learning React is that typically the first thing you’re taught are components. “Components are the building blocks of React”. Notice, however, that we started this post with elements. The reason for this is because once you understand elements, understanding components is a smooth transition. A component is a function or a Class which optionally accepts input and returns a React element.
function Button ({ onLogin }) {
return React.createElement(
'div',
{id: 'login-btn', onClick: onLogin},
'Login'
)
}
By definition, we have a Button
component which accepts an onLogin
input and returns a React element. One thing to note is that our Button
component receives an onLogin
method as its prop. To pass that along to our object representation of the DOM, we pass it along as the second argument to createElement
, just as we did our id
attribute.
Let’s go deeper.
Up until this point we’ve only covered creating React elements with the type
property of native HTML elements ( span
, div
, etc), but you can also pass in other React components to the first argument of createElement
.
const element = React.createElement(
User,
{name: 'Tyler McGinnis'},
null
)
However, unlike with an HTML tag name, if React sees a class or a function as the first argument, it will then check to see what element it renders, given the corresponding props. React will continue to do this until there are no more createElement
invocations which have a class or a function as their first argument. Let’s take a look at this in action.
function Button ({ addFriend }) {
return React.createElement(
"button",
{ onClick: addFriend },
"Add Friend"
)
}
function User({ name, addFriend }) {
return React.createElement(
"div",
null,
React.createElement(
"p",
null,
name
),
React.createElement(Button, { addFriend })
)
}
Above we have two components. A Button
and a User
. User
’s object representation of the DOM will be a div
with two children, a p
which wraps the user’s name
and a Button
component. Now, let’s swap out the createElement invocations with what they return,
function Button ({ addFriend }) {
return {
type: 'button',
props: {
onClick: addFriend,
children: 'Add Friend'
}
}
}
function User ({ name, addFriend }) {
return {
type: 'div',
props: {
children: [
{
type: 'p',
props: {
children: name
}
},
{
type: Button,
props: {
addFriend
}
}
]
}
}
}
You’ll notice in the above code we have four different type properties, button
, div
, p
, and Button
. When React sees an element with a function or class type (like our type: Button
above), it will then consult with that component to know which element it returns, given the corresponding props. With that in mind, at the end of this process, React has a full object representation of the DOM tree. In our example, that will look like this,
{
type: 'div',
props: {
children: [
{
type: 'p',
props: {
children: 'Tyler McGinnis'
}
},
{
type: 'button',
props: {
onClick: addFriend,
children: 'Add Friend'
}
}
]
}
}
This whole process is called reconciliation in React and it’s triggered every time setState
or ReactDOM.render
is called.
So now let’s again take a look at our initial question that sparked this blog post,
// Function Definition
function add (x, y) {
return x + y
}
// Function Invocation
add(1,2)
// Component Definition
function Icon () {}
// Component Invocation???
<Icon />
At this point, we have all the knowledge we need to answer this question except for one crucial piece. Odds are, if you’ve been using React for any amount of time, you don’t use React.createElement
to create your object representations of the DOM. Instead, you’re probably using JSX. Earlier I wrote, “The primary reason for the confusion is that there’s an often un-talked about abstraction layer between JSX and what’s actually going on in React land”. This abstraction layer is that JSX is always going to get compiled to React.createElement
invocations (typically) via Babel.
Looking at our earlier example, this code
function Button ({ addFriend }) {
return React.createElement(
"button",
{ onClick: addFriend },
"Add Friend"
)
}
function User({ name, addFriend }) {
return React.createElement(
"div",
null,
React.createElement(
"p",
null,
name
),
React.createElement(Button, { addFriend })
)
}
is the result of this JSX being compiled.
function Button ({ addFriend }) {
return (
<button onClick={addFriend}>Add Friend</button>
)
}
function User ({ name, addFriend }) {
return (
<div>
<p>{name}</p>
<Button addFriend={addFriend}/>
</div>
)
}
So finally, what do we call it when we write out our component like this, <Icon/>
? We can call it “creating an element” because after the JSX is compiled, that’s exactly what’s happening.
React.createElement(Icon, null)
All of these examples are “creating a React element”.
React.createElement(
'div',
{ className: 'container' },
'Hello!'
)
<div className='container'>Hello!</div>
<Hello />