React
- Development environment
- Boilerplate
- JSX
- Components all teh things
- DOM control
- Styling
- Rendering Lists
- Handling Events
- State Management
- Passing Parameters
- Component Composition
- Passing Data
- Three Rules about State
- Raising and Handling Events between Components
- Synchronising Components
- Stateless Functional Components
- Destructuring Data (Arguments)
- Functional components
- Class components
- Life cycle Hooks
- Effects (hooks)
- Debugging
- React Router
- Testing
- Production
- Troubleshooting
Development environment⌗
- Install Node. Use nvm or nvm-windows(https://github.com/coryhouse/pluralsight-redux-starter/archive/master.zip) if multiple versions are required.
- Jump into the project root, and install node packages
npm install
- Install Chrome devtools for React developer tools and Redux Dev Tools.
Useful vscode extensions:
- prettier - code formatting (enable the format on save setting)
- ESLint - get red swigglys under source code
Boilerplate⌗
So many options to bootstrap a new react project. Setting everything up manually is best, as it builds intuition about the pieces of infrastructure.
Some useful features in a JavaScript development environment that I’ll configure:
- linting
- code formatting
- bundling and packing
- ES6+ support
- local web server
Webpack⌗
Webpack for bundling grunt work and the local http server:
Webpack can transform front-end assets like HTML, CSS, and images if the corresponding loaders are included. webpack takes modules with dependencies and generates static assets representing those modules.
In the project root webpack.config.dev.js
:
// node lacks support for ESM imports yet, stick with commonjs imports
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
process.env.NODE_ENV = "development";
module.exports = {
mode: "development",
target: "web", //web = browser, node = backend
devtool: "cheap-module-source-map", // source maps expose the original un-transpiled code for debugging
entry: "./src/index", // app entry point (can omit extension)
output: {
// in development mode webpack keeps everything in-memory
path: path.resolve(__dirname, "build"), // the route to serve the in-memory assets
publicPath: "/", // the URI to serve in-memory assets to browser
filename: "bundle.js", // the fake name that can be used to reference the in-memory bundle
},
devServer: {
stats: "minimal", // reduce verbosity of stdout logs
overlay: true, // overlay any errors that occur in the browser,
historyApiFallback: true, // route all requests through index.html, to do deep linking with react-router
disableHostCheck: true, //HACK: chrome bug
headers: { "Access-Control-Allow-Origin": "*" }, //HACK: chrome bug
https: false, //HACK: chrome bug
},
plugins: [
new HtmlWebpackPlugin({
template: "src/index.html",
favicon: "src/favicon.ico",
}),
],
module: {
// what files should webpack handle?
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ["babel-loader"], // run babel on all js/jsx files before bundling
},
{
test: /(\.css)$/,
use: ["style-loader", "css-loader"], // webpack will bundle any css imported by js
},
],
},
};
This inline comments should explain most things, but one to draw out is cheap-module-source-map
which exposes the original code, when developing and debugging. Try throwing the debugger
keyword in some js, and refresh with chrome devtools open. The original source code will appear, with a message in the toolbar source mapped from bundle.js. Cool!
Babel⌗
Babel for transpiling modern ES6 to ES5 for wider browser support:
Babel is a free and open-source JavaScript transcompiler that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript that can be run by older JavaScript engines. Babel is a popular tool for using the newest features of JavaScript.
Babel can be configured via .babelrc
or package.json
(under the key of babel
):
"babel": {
"presets": [
"babel-preset-react-app"
]
}
The babel-preset-react-app
preset, will transpile JSX and modern ES features such as object spreading, class props, dynamic imports and more.
NPM scripts⌗
A general purpose task runner, available as the scripts
key in package.json
:
"scripts": {
"start": "webpack-dev-server --config webpack.config.dev.js --port 3000"
},
Running npm start
on the CLI will launch this code.
ESLint⌗
Linters are useful for provided fast feedback of potential issues at development time, speeding up the dev/deploy/test cycle. ESLint is a popular choice for JavaScript and JSX:
A static code analysis tool for identifying problematic patterns found in JavaScript code.
Like babel, can be configured in a standalone file, but also supports being configured via package.json
under the eslintConfig
key:
"eslintConfig": {
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"browser": true,
"node": true,
"es6": true,
"jest": true
},
"rules": {
"no-debugger": "off",
"no-console": "off",
"no-unused-vars": "warn",
"react/prop-types": "warn"
},
"settings": {
"react": {
"version": "detect"
}
},
"root": true
}
Some notable features:
- Plug-in around react best practices.
- The
babel-eslint
parser provides strong compatibility with babel generated JavaScript, including support for ES2018, ESM (EcmaScript Modules) and JSX. env
tells ESLint to ignore use of global variables for well known packages that you intend to use.rules
relaxes the strictness around useful things likeconsole.log
anddebugger
root
makes this the base config, overriding settings in any user local ESLint configs.
The ESLint CLI can be manually run, however, getting the webpack watcher to automatically run ESLint is very convenient:
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ["babel-loader", "eslint-loader"], // run babel on all js/jsx files before bundling
}
]
}
Test things out by trying (for example) to add a global variable somewhere, as soon as the file is saved:
Module Error (from ./node_modules/eslint-loader/index.js):
/home/ben/code/react-playground/boilerplate/src/index.js
4:1 error 'myGlobal' is not defined no-undef
✖ 1 problem (1 error, 0 warnings)
Production dependencies⌗
Dependency | Use |
---|---|
bootstrap | CSS Framework |
immer | Helper for working with immutable data |
prop-types | Declare types for props passed into React components |
react | React library |
react-dom | React library for DOM rendering |
react-redux | Connects React components to Redux |
react-router-dom | React library for routing |
react-toastify | Display messages to the user |
redux | Library for unidirectional data flows |
redux-thunk | Async redux library |
reselect | Memoize selectors for performance |
Development dependencies⌗
Dependency | Use |
---|---|
@babel/core | Transpiles modern JavaScript so it runs cross-browser |
babel-eslint | Lint modern JavaScript via ESLint |
babel-loader | Add Babel support to Webpack |
babel-preset-react-app | Babel preset for working in React. Used by create-react-app too. |
css-loader | Read CSS files via Webpack |
cssnano | Minify CSS |
enzyme | Simplified JavaScript Testing utilities for React |
enzyme-adapter-react-16 | Configure Enzyme to work with React 16 |
eslint | Lints JavaScript |
eslint-loader | Run ESLint via Webpack |
eslint-plugin-import | Advanced linting of ES6 imports |
eslint-plugin-react | Adds additional React-related rules to ESLint |
fetch-mock | Mock fetch calls |
html-webpack-plugin | Generate HTML file via webpack |
http-server | Lightweight HTTP server to serve the production build locally |
jest | Automated testing framework |
json-server | Quickly create mock API that simulates create, update, delete |
mini-css-extract-plugin | Extract imported CSS to a separate file via Webpack |
node-fetch | Make HTTP calls via fetch using Node - Used by fetch-mock |
npm-run-all | Display results of multiple commands on single command line |
postcss-loader | Post-process CSS via Webpack |
react-test-renderer | Render React components for testing |
react-testing-library | Test React components |
redux-immutable-state-invariant | Warn when Redux state is mutated |
redux-mock-store | Mock Redux store for testing |
rimraf | Delete files and folders |
style-loader | Insert imported CSS into app via Webpack |
webpack | Bundler with plugin ecosystem and integrated dev server |
webpack-bundle-analyzer | Generate report of what’s in the app’s production bundle |
webpack-cli | Run Webpack via the command line |
webpack-dev-server | Serve app via Webpack |
JSX⌗
In React, everything is just Javascript. JSX takes this to the next level, providing syntactic sugar to crunch out lots of React.createElement
, based on a HTML-esk syntax. babel.js takes care of the transpilation from HTML looking tags to actual Javascript. To get a real sense of this, go to babeljs.io/repl and type in const element = <h1>hello world</h1>;
. You’ll see this get translated to:
"use strict";
var element = React.createElement("h1", null, "hello world");
Components all teh things⌗
Within the ./src/components
dir, create new jsx file to represent the component.
import React, { Component } from "react";
class Counter extends Component {
//state = {}
render() {
return (
<React.Fragment>
<h1>hello world</h1>
<button>Increment</button>
</React.Fragment>
);
}
}
export default Counter;
DOM control⌗
JSX expressions must have a single parent element, because babel needs a single React.createElement
parent, before populating it with many child elements. Using a single outer <div
would be one option, but if you really don’t want to include any outer DOM, can use the React.Fragment
shown above.
Styling⌗
Because JSX needs to boil down into JS, reserved JS keywords like class
cannot be used in JSX. To set the class of an element, must use className
.
<React.Fragment>
<img src={this.state.imageUrl} alt="" />
<span className="">{this.formatCount()}</span>
<button>Increment</button>
</React.Fragment>
While using CSS classes is best, inline styles can be acheived by setting the style
attribute on a JSX element to a Javascript object, like so:
styles = {
fontSize: 12,
fontWeight: "bold"
};
render() {
return (
<React.Fragment>
<span style={this.styles} className="badge badge-primary m-2">
{this.formatCount()}
</span>
Or using an anonymous object like this:
<span style={{ fontSize: 30 }} className="badge badge-primary m-2">
Rendering Lists⌗
The map
(higher order) function, can be used to deal with lists:
class Counter extends Component {
state = {
count: 0,
tags: ["tag1", "tag2", "tag3"]
};
render() {
return (
<React.Fragment>
<ul>
{this.state.tags.map(tag => (
<li>{tag}</li>
))}
</ul>
This will render a list of naked li
, however React will throw a warning in the console:
Warning: Each child in a list should have a unique “key” prop.
In order to do virtual DOM to DOM comparison, React needs unique identifiers on everything. As there is no id
on these li
’s, the key
attribute can be used:
<li key={tag}>{tag}</li>
Handling Events⌗
JSX provides event handler attributes such as onClick
:
handleIncrement() {
console.log("Increment clicked", this.state.count);
}
render() {
return (
<React.Fragment>
<button
onClick={this.handleIncrement}
className="btn btn-secondary btn-sm"
>
Increment
</button>
State Management⌗
In the above click handler, the console.log
fails with:
TypeError: this is undefined
this
in JS is context dependent. If a method is called on an object obj.method()
then this
is a reference to that object. However if a function is called statically i.e. function();
then this
is a reference to the window object, or in strict mode undefined
.
React rule of thumb:
The component that owns some state, should be responsible for mutating it.
Option 1: Binding⌗
In the constructor of the component, after calling super()
can bind this
to the static function like so:
constructor() {
super();
this.handleIncrement = this.handleIncrement.bind(this);
}
this
is now available to the event handler function.
Option 2: Arrows⌗
Just use an arrow function:
handleIncrement = () => {
console.log("Increment clicked", this.state.count);
};
Now that the event handler has this
access, cannot just start mutating it, for example this.state.count++
will not affect the UI, as React is not aware that this custom piece of state has changed, and what is impacted by the state change.
To change state, React provides setState
:
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
Passing Parameters⌗
It is common to want to pass some state along to the event handler, when its invoked.
One option is to create a wrapper function:
handleIncrement = product => {
console.log(product);
this.setState({ count: this.state.count + 1 });
};
doHandleIncrement = () => {
this.handleIncrement({ id: 1 });
};
render() {
return (
<React.Fragment>
<button
onClick={this.doHandleIncrement}
className="btn btn-secondary btn-sm"
>
Increment
</button>
For consiseness wrapper function can be represented as an inline (arrow) function:
<button
onClick={() => this.handleIncrement({ id: 1 })}
className="btn btn-secondary btn-sm"
>
Increment
</button>
Component Composition⌗
A React app is a tree of components. In index.js
is kicked off with the:
ReactDOM.render(<Counter />, document.getElementById("root"));
To compose many Counter
components, one option is to create an outer Counters
component:
import React, { Component } from "react";
import Counter from "./counter";
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
],
};
render() {
return (
<div>
{this.state.counters.map((c) => (
<Counter key={c.id} />
))}
</div>
);
}
}
export default Counters;
Passing Data⌗
While the above Counters
component successfully composes several Counter
’s from a list, it does not pass state to the child components.
State can be propagated to child components, as attribute like so:
<Counter key={c.id} value={c.value} selected={true} />
This state is exposed to the target components as props
. A console log in child component Counter
render method:
render() {
console.log("props", this.props);
Results in:
props Object { value: 0, selected: true, … }
props Object { value: 0, selected: true, … }
props Object { value: 3, selected: true, … }
props Object { value: 0, selected: true, … }
props
can be used throughout the component, and even directly in the component state
, like so:
class Counter extends Component {
state = {
count: this.props.value,
tags: ["tag1", "tag2", "tag3"],
imageUrl: "https://picsum.photos/200",
address: {
street: ""
}
};
Passing Children (passing down JSX)⌗
Previously the Counters
component, composed many Counter
instances, like so, while defining a few props
:
<div>
{this.state.counters.map(c => (
<Counter key={c.id} value={c.value} selected={true} />
))}
</div>
To make components even more useful, its possible to pass in inner JSX content, for example:
<Counter key={c.id} value={c.value} selected={true}>
<h2>semaphore</h2>
</Counter>
The props
of the child component, exposes this h2
via a list called children
:
props {…}
children: {…}
"$$typeof": Symbol(react.element)
_owner: Object { tag: 1, key: null, index: 0, … }
_self: Object { props: {}, context: {}, refs: {}, … }
_source: Object { fileName: "./src/components/counters.jsx", lineNumber: 19 }
_store: Object { … }
key: null
props: Object { children: "semaphore" }
ref: null
type: "h2"
<prototype>: Object { … }
key:
selected: true
value: 0
<get key()>: function warnAboutAccessingKey()
<prototype>: {…
To make use of the props.children
property is easy:
render() {
return (
<React.Fragment>
{this.props.children}
<span className={this.getBadgeClasses()}>{this.formatCount()}</span>
This will render the children as defined in the calling component, in this case an h2
tag will be emitted.
Interestingly props
is immutable, and can’t be modified, state
should be used.
Three Rules about State⌗
Rule one, never modify state
directly, always use setState()
.
Rule two, state updates are asynchronous, as a result the correct values of this.props
and this.state
at the time they are queried, can’t always be guaranteed. The second form of setState()
that accepts a function (not the usual object) should be used.
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
Rule three, state updates are magically merged. State might contain several variables:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
Individual variables can be updated independently, with separate setState()
calls, without clobbering.
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
Raising and Handling Events between Components⌗
State management rule of thumb:
The component that owns some state, should be responsible for mutating it.
A common scenario is for parent or outer components to define the state that is consumed by downstream components. Given the rule of thumb above, how should a downstream component go about modifying the original state?
One good option is the observer pattern, in which a child component raises an event, which can be handled by its parent component to react and respond to a state change.
For example, the Counters
component defines several Counter
components. A component can elect to delete itself by displaying a delete button to the user. If clicked, the Counter
instance could raise an onDelete
event. Counters
could handle this event with handleDelete()
.
In the Counters
component:
-
create event handler e.g.
handleDelete
responsible for mutating the original state that relates to the event. important in React, you can’t just mutate state and expect React to know about it, instead you must reassign the state using ReactssetState
function (see below) -
pass this handler function downstream to
Counter
components asprops
class Counters extends Component { state = { counters: [ { id: 1, value: 0 }, { id: 2, value: 0 }, { id: 3, value: 3 }, { id: 4, value: 2 } ] };
handleDelete = counterId => { console.log("Event handler called", counterId); const counters = this.state.counters.filter(c => c.id !== counterId); this.setState({ counters: counters }); }; render() { return ( <div> {this.state.counters.map(c => ( <Counter key={c.id} value={c.value} onDelete={this.handleDelete} selected={true} /> ))} </div>
Using the React Developer Tools browser extension, can drill down into Counters
to an individual Counter
instance, and check out its Props (right hand panel) - note the onDelete
prop:
Props
onDelete: handleDelete()
selected: true
value: 0
In the Counter
component, simply need to wire in the onDelete
now available in props
to the onClick
:
<button
onClick={() => this.props.onDelete(this.props.id)}
className="btn btn-danger btn-sm m-2"
>
Passing Object props⌗
Over time, the number of individual props
defined may continue to swell, like so:
<Counter
key={c.id}
value={c.value}
onDelete={this.handleDelete}
selected={true}
/>
Instead, of mapping each field as a prop, why not pass the entire object (in this case called counter
) along:
<Counter
key={counter.id}
counter={counter}
/>
Make sure to update affected props
reference in the child components:
class Counter extends Component {
state = {
count: this.props.counter.value,
...
Controlled Components⌗
Local state within components, can cause components to get out of wack. For example, when setting the initial state
of a child component from props
only happens when the component is created. Future changes to state in the parent, that was passed down as props
to its children, will not flow through after initial creation.
A controlled component is one that:
- Receives essential data needed to render via
props
from its parent. - Never manages this as
state
locally, but instead fires events to request the data be updated back in the parent.
Refactoring steps:
- Remove any local
state
- Any references to
this.state
should be updated to useprops
directly, and trigger events to communicate with the parent (observer) where needed.
For example, this button now delegates its click handler to a func passed through by its housing (parent) component:
<button
onClick={() => this.props.onIncrement(this.props.counter)}
className="btn btn-secondary btn-sm"
>
Synchronising Components⌗
Sometimes, as the architecture of the view changes, new components get introduced. This can modify the shape of the component tree.
For example, introducing a navbar component that requires access to the state of another unrelated component.
The general pattern for this is known as lifting the state upwards. The higher in the tree the state lives, the more widely it can be propagated through other downstream components.
The Counters
component used to be responsible for managing the counters
in its own state
, rendering each child Counter
component. Lifting this state upwards to top level App
component, means refactoring this.state
access with this.props
accessors. Event handlers must bubble upwards too:
<Counter
key={c.id}
counter={c}
onDelete={this.props.onDelete}
onIncrement={this.props.onIncrement}
selected={true}
In the top level App
component, the render can now propagate state as needed, for example only passing the totalCounters
to the NavBar
component, while passing down the full counter objects to Counters
:
render() {
return (
<React.Fragment>
<NavBar
totalCounters={this.state.counters.filter(c => c.value > 0).length}
/>
<main className="container">
<Counters
counters={this.state.counters}
onReset={this.handleReset}
onDelete={this.handleDelete}
onIncrement={this.handleIncrement}
/>
</main>
</React.Fragment>
);
}
Stateless Functional Components⌗
When a component is simple enough to contain only a render
. No supporting funcs or logic. No object state (i.e. props
only access to data).
The following NavBar component is a prime candidate for conversion:
class NavBar extends Component {
render() {
return (
<nav className="navbar navbar-light bg-light">
<a className="navbar-brand" href="#">
Cool React App{" "}
<span className="badge badge-pill badge-secondary">
{this.props.totalCounters}
</span>
</a>
</nav>
);
}
}
Converting a class based component to functional, involves simply defining the component as an arrow function that returns a JSX element. Any use of object this
state such as this.props
, need to be defined as parameters on the arrow function (props
will be dependency injected automatically).
const NavBar = (props) => {
return (
<nav className="navbar navbar-light bg-light">
<a className="navbar-brand" href="#">
Cool React App{" "}
<span className="badge badge-pill badge-secondary">
{props.totalCounters}
</span>
</a>
</nav>
);
};
Destructuring Data (Arguments)⌗
Reacts component model and one way binding means that data and callbacks get passed around allot, as data (props
) is propagated down the component hierarchy.
const NavBar = (props) => {
return (
<nav className="navbar navbar-light bg-light">
<a className="navbar-brand" href="#">
Cool React App{" "}
<span className="badge badge-pill badge-secondary">
{props.totalCounters}
</span>
To simplify the constant need to prefix props.
, can leverage ES6 argument destructuring to tear apart the individual data fields.
const NavBar = ({totalCounters}) => {
return (
<nav className="navbar navbar-light bg-light">
<a className="navbar-brand" href="#">
Cool React App{" "}
<span className="badge badge-pill badge-secondary">
{totalCounters}
</span>
Which is equivalent to:
const NavBar = (props) => {
const { totalCounters } = props;
return (
...
Functional components⌗
const NavBar = (props) => {
becomes const NavBar = ({ totalCounters }) => {
Resulting in cleaner prop
access:
const NavBar = ({ totalCounters }) => {
return (
<nav className="navbar navbar-light bg-light">
<a className="navbar-brand" href="#">
Cool React App{" "}
<span className="badge badge-pill badge-secondary">
{totalCounters}
</span>
</a>
</nav>
);
};
Class components⌗
Involves ripping apart the props
at the top of the render
:
class Counters extends Component {
render() {
return (
<div>
<button
onClick={this.props.onReset}
className="btn btn-primary btn-sm m-2"
>
Reset
</button>
{this.props.counters.map((c) => (
<Counter
key={c.id}
counter={c}
onDelete={this.props.onDelete}
onIncrement={this.props.onIncrement}
selected={true}
/>
))}
</div>
);
}
}
After:
class Counters extends Component {
render() {
const { onReset, counters, onDelete, onIncrement } = this.props;
return (
<div>
<button onClick={onReset} className="btn btn-primary btn-sm m-2">
Reset
</button>
{counters.map((c) => (
<Counter
key={c.id}
counter={c}
onDelete={onDelete}
onIncrement={onIncrement}
selected={true}
/>
))}
</div>
);
}
}
Life cycle Hooks⌗
Allow activities to be performed when interesting events occur. Some interesting life cycle events available:
Phase 1 the birth phase (mount)⌗
When an instance of a component is first created and injected in the DOM. Available hooks in this phase include:
constructor
component constructorrender
the component itself, and all its childrencomponentDidMount
after component output is rendered into DOM, perfect doing AJAX laundry or setup of things like timers (setInterval
)
The constructor
can access props
only if its defined as parameter in constructor (and DI injected).
constructor() {
super();
console.log('App - Constructor', this.props); //'App - Constructor > undefined'
}
Dependency injected props
:
constructor(props) {
super(props);
console.log('App - Constructor', this.props); //'App - Constructor > Object { }'
}
At constructor time, setState
is not usable. This is the only time it is acceptable to set this.state
directly, for example:
this.state = {
posts: [],
comments: []
}
Phase 2 the update phase⌗
When the state
or props
of a component are modified. Available hooks here:
render
componentDidUpdate
called after a component is updated, which means there is some new state or props. The previous version of the state or props can be compared to the latest state or props, and if necessary necessary work such as firing off new ajax fetches, and so on (this is a nice optimisation technique).
componentDidUpdate
provides the prevProps and prevState values, which is useful to detect is a piece of data has changed that is worthy of other work (such as a fresh ajax call to the server):
componentDidUpdate(prevProps, prevState) {
console.log('prevProps', prevProps);
console.log('prevState', prevState);
if (prevProps.counter.value !== this.props.counter.value) {
// ajax fetch new data from server
}
}
Phase 3 the death phase (unmount)⌗
When a component life comes to an end (e.g. the removal of a Counter component).
componentWillUnmount
fired as soon as a component is removed from the DOM, perfect for tearing down resources such as timers or listeners.
Effects (hooks)⌗
Effects were introduced in early 2019, and give function components the same degree of hooks as class components.
Class components are no longer necessary, but are still supported.
A class component (before):
class ManageCoursesPage extends React.Component {
componentDidMount() {
const { courses, authors, loadCourses, loadAuthors } = this.props;
if (courses.length === 0) {
loadCourses().catch((error) => {
alert("Loading courses failed " + error);
});
}
if (authors.length === 0) {
loadAuthors().catch((error) => {
alert("Loading authors failed " + error);
});
}
}
render() {
return (
<>
<h2>Manage Course</h2>
</>
);
}
}
Converted to a functional component with effects (after):
import React, { useEffect } from "react";
...
function ManageCoursesPage({ courses, authors, loadCourses, loadAuthors }) {
useEffect(() => {
if (courses.length === 0) {
loadCourses().catch((error) => {
alert("Loading courses failed " + error);
});
}
if (authors.length === 0) {
loadAuthors().catch((error) => {
alert("Loading authors failed " + error);
});
}
}, []);
// The 2nd param is list of items to monitor, if they change, fire the effect.
// An empty array causes the effect to only fire once (i.e. like componentDidMount)
return (
<>
<h2>Manage Course</h2>
</>
);
}
Some highlights:
- The props destructuring happens right in the function signature.
componentDidMount
is replaced withuseEffect
- No need for an explicit
render
, as its implied in a functional component
Debugging⌗
Get the React Developer Tools Chrome or FF extension.
Using the newly provided React tab within dev tools, will now be able to clearly view the tree hierarchy of React components.
Component in this view are selectable, and will be assigned to variable $r
, like so:
<Counters>
<div>
<Counter key="1" value={0} selected={true}>...</Counter> == $r
<Counter key="2" value={0} selected={true}>...</Counter>
<Counter key="3" value={3} selected={true}>...</Counter>
<Counter key="4" value={2} selected={true}>...</Counter>
</div>
</Counters>
In the same way selecting a piece of DOM with the Elements tab would assign it to $0
. Using the $r
reference in the Console can begin to inspect and play with it:
- call its
render()
React Router⌗
SPA web frameworks render components based on some state, making them appear like multiple pages. This is not the case.
React Router adds the notion of conditionally rendering components based on the route specified in the URI, such as /
for home, /about
for the about page, and so on.
Benefits of faking routes and pages in a SPA:
- aids crawlers (SEO) to walk the site hierarchy,
- bookmarkable URL’s,
- reduces the initial design overhead of planning out the state/props to facilitate component rendering (i.e. can lean on the router)
React Router makes this possible by provides a set of navigation components:
-
BrowserRouter
-
HashRouter
-
Link
provides declarative navigation around your application. Note the use of backticks for setting theto
prop with actual state:
Install the package:
$ yarn install react-router-dom
Import to the (top level) component responsible for rendering the content portion of the app (e.g. App.js
):
import { BrowserRouter } from 'react-router-dom'
Or optionally alias it to the slicker Router
:
import { BrowserRouter as Router } from 'react-router-dom'
Wrap everything in the render()
with the BrowserRouter
component:
render() {
return (
<BrowserRouter>
<NavBar
totalCounters={this.state.counters.map(a => a.value).reduce((accumulator, currentValue) => accumulator + currentValue)}
/>
<div className="d-flex" id="wrapper">
<SideBar />
<div id="page-content-wrapper">
<div className="container-fluid">
<Counters
counters={this.state.counters}
onReset={this.handleReset}
onDelete={this.handleDelete}
onIncrement={this.handleIncrement}
/>
</div>
</div>
</div>
</BrowserRouter>
);
}
Roll out the Link
component, to parts of the UI that should influence the routes (e.g. a sidebar or nav menu for changing pages). Here is my custom SideBar
component:
render() {
const { gists } = this.state;
return (
<div className="bg-light border-right" id="sidebar-wrapper">
<div className="sidebar-heading">Router Fun</div>
<div className="list-group list-group-flush">
{ gists ? (
gists.filter(gist => gist.description !== '').map(gist => (
<Link to={`/g/${gist.id}`} className="list-group-item list-group-item-action bg-light">
{gist.description}
</Link>
))
) : (
<div>Loading...</div>
)}
</div>
</div>
);
}
Now back in the top level component using the BrowserRouter
component, register some Route
s:
render() {
return (
<BrowserRouter>
<React.Fragment>
<div className="d-flex" id="wrapper">
<SideBar gists={this.state.gists} />
<div id="page-content-wrapper">
<div className="container-fluid">
<Route path="/g/:gistId" component={Gist}></Route>
Clicking a Link
will change the URI to something like /g/77501744874a981e61c86ba3edfb4a46
(the gist id), the route will match, which will render the Gist
component:
import React, { Component } from "react";
const Gist = ({ match }) => (
<div className="container">
<h1>{match.params.gistId}</h1>
</div>
);
export default Gist;
I want a home page to render at /
. Back in the BrowserRouter
, register a new /
Route
:
<Route
path="/"
exact={true}
render={(props) => (
<Home
{...props}
counters={this.state.counters}
onReset={this.handleReset}
onDelete={this.handleDelete}
onIncrement={this.handleIncrement}
/>
)}
/>
This is a render
route, unlike the previous component route that was used. As you can see, allows you to bind custom props, in addition to the 3 route props (preserved with the spread operator). Note, the exact
prop here prevents this route from matching whenever there is a leading forward slash /
(basically every URL). Exact, you guessed it, forces an exact match of a single /
. Working this second home route into the existing BrowserRouter
:
render() {
return (
<BrowserRouter>
<React.Fragment>
<div className="d-flex" id="wrapper">
<SideBar gists={this.state.gists} />
<div id="page-content-wrapper">
<div className="container-fluid">
<Route path="/" exact={true} render={(props) =>
<Home {...props}
counters={this.state.counters}
onReset={this.handleReset}
onDelete={this.handleDelete}
onIncrement={this.handleIncrement} />} />
<Route path="/g/:gistId" component={Gist}></Route>
react-router also features a redirect
component.
import { Redirect } from "react-router-dom";
class CoursesPage extends React.Component {
state = {
redirectToAddCoursePage: false,
};
render() {
return (
<>
{this.state.redirectToAddCoursePage && <Redirect to="/course" />}
<h2>Courses</h2>
<button
style={{ marginBottom: 20 }}
className="btn btn-primary add-course"
onClick={() => this.setState({ redirectToAddCoursePage: true })}
>
Add Course
</button>
<CourseList courses={this.props.courses} />
</>
);
}
}
It’s also possible to use react-routers history to change the URL to a location. Any component routed by react-router
will automatically be injected with a history
prop.
Before:
function handleSave(event) {
event.preventDefault();
saveCourse(course);
}
After:
function handleSave(event) {
event.preventDefault();
saveCourse(course).then(() => {
history.push("/courses");
});
}
Testing⌗
React by design is incredibly testable, something that was difficult in traditional server-side technologies. For example, its possible to render a single component against an in-memory DOM and fake click events, to test a component behaves as expected.
Like everything in the react ecosystem, there are lots of choices.
Popular testing frameworks include jest (facebook and bundled with CRA), mocha (very configurable with large ecosystem), jasmine (like mocha but slighly less configurable), tape (minimalist) and AVA.
Helper libraries exist to aid verification of components, such as mocking and shallow rendering (not rendering the child hierarchy). Popular options include ReactTestUtils, Enzyme and React testing library.
Jest⌗
Setup an npm script runner for jest:
{
"name": "ps-redux",
"description": "React and Redux Pluralsight course by Cory House",
"scripts": {
"start": "run-p start:dev start:api",
"start:dev": "webpack-dev-server --config webpack.config.dev.js --port 3000",
"prestart:api": "node tools/createMockDb.js",
"start:api": "node tools/apiServer.js",
"test": "jest --watch"
},
"jest": {
"setupFiles": [
"./tools/testSetup.js"
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/tools/fileMock.js",
"\\.(css|less)$": "<rootDir>/tools/styleMock.js"
}
},
The fileMock.js
and styleMock.js
provide a testing hook for when files of a particular type get consumed by the rendering process.
Jest automatically searches for tests in files that end in .test.js
or .spec.js
, here’s a test in a file called index.test.js
:
it("should pass", () => {
expect(true).toEqual(true);
});
Snapshot testing with jest, captures the rendered output of components, and compares the output over time to ensure regressions havent slipped in.
A benefit of setting up mockdata with something like JSON Server is that the same mock data can be used in the unit tests.
import React from "react";
import CourseForm from "./CourseForm";
import renderer from "react-test-renderer";
import { courses, authors } from "../../../tools/mockData";
it("sets submit button label to 'Saving...' when saving prop is true", () => {
const tree = renderer.create(
<CourseForm
course={courses[0]}
authors={authors}
onChange={jest.fn()}
onSave={jest.fn()}
saving={true}
/>
);
expect(tree).toMatchSnapshot();
});
it("sets submit button label to 'Save' when saving prop is false", () => {
const tree = renderer.create(
<CourseForm
course={courses[0]}
authors={authors}
onChange={jest.fn()}
onSave={jest.fn()}
saving={false}
/>
);
expect(tree).toMatchSnapshot();
});
Running this will result in jest creating an __snapshots__
directory next to the test file, with the output of the rendered DOM.
jest.fn()
creates an empty function, to save you the effort, so you can stub any function props.
Now with the snapshots captured, these could be committed into your git repo, and verified as part of your CI pipeline. If for example, the label of the save button changed or the logic basec on the saving
prop broken the snapshot test would fail.
Jest in watch mode provides a bunch of options:
Watch Usage
- Press a to run all tests.
- Press f to run only failed tests.
- Press p to filter by a filename regex pattern.
- Press t to filter by a test name regex pattern.
- Press q to quit watch mode.
- Press u to update failing snapshots.
- Press Enter to trigger a test run.
React Test Utils⌗
Takes two approaches to component rendering; shallowRender
and renderIntoDocument
.
shallowRender
will render only a single component (not child components) which is useful for asserting the coupling to other components and has no dependency on an actual DOM. Not having a DOM doesn’t work for all scenarios, particularly things like button click events.
renderIntoDocument
will do a full component (and its children) render, and requires a DOM. Requiring a DOM doesn’t necessary mean a browser however, with headless DOM libs like jsdom.
jsdom is a pure-JavaScript implementation of many web standards, for use with Node.js
Some examples of its API are scryRenderedDOMComponentsWithClass()
, findRenderedDOMComponentWithClass()
, scryRenderedDOMComponentsWithTag()
, findRenderedDOMComponentWithTag()
. The scry
variants finds all DOM elements of components in the rendered tree that match, while the find
variants expects only a single result.
The Simulate
function allows you to fake DOM events such as key pressing or clicking an element:
const node = this.button;
ReactTestUtils.Simulate.click(node);
Enzyme⌗
An wrapper by airbnb that includes ReactTestUtils
, JSDOM
(in-memory DOM) and Cheerio (CSS selector support), provides a simplified, headless testing environment and API. For example, it provides a single find
function that accepts CSS style selectors.
describe('<MyComponent />', () => {
it('renders three <Foo /> components', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo)).to.have.lengthOf(3);
});
Enzyme offers a few rending methods:
- Shallow rendering:
shallow()
, no DOM, no child components, fast - Full DOM rendering:
mount()
, in-memory DOM with JSDOM, child components rendered, slow - Static rendering
To setup enzyme an adapter specific to the version of react you’re running needs to be bootstrapped ./tools/testSetup.js
:
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() });
Configure jest to hook this in package.json
:
"jest": {
"setupFiles": [
"./tools/testSetup.js"
],
Create a file that ends in .test.js
for jest to pick it up, putting enzyme in the name is a nice touch ex: CourseForm.Enzyme.test.js
:
import React from "react";
import CourseForm from "./CourseForm";
import { shallow } from "enzyme";
function renderCourseForm(args) {
const defaultProps = {
authors: [],
course: {},
saving: false,
errors: {},
onSave: () => {},
onChange: () => {},
};
const props = { ...defaultProps, ...args };
return shallow(<CourseForm {...props} />);
}
it("renders form and header", () => {
const wrapper = renderCourseForm();
// console.log(wrapper.debug());
expect(wrapper.find("form").length).toBe(1);
expect(wrapper.find("h2").text()).toEqual("Add Course");
});
it('labels save buttons as "Save" when not saving', () => {
const wrapper = renderCourseForm();
expect(wrapper.find("button").text()).toBe("Save");
});
it('labels save button as "Saving..." when saving', () => {
const wrapper = renderCourseForm({ saving: true });
expect(wrapper.find("button").text()).toBe("Saving...");
});
The renderCourseForm()
cuts down on props wrangling boilerplate code, that each test would need to setup.
Finally to contrast shallow versus full DOM rendering that enzyme supports:
import React from "react";
import Header from "./Header";
import { mount, shallow } from "enzyme";
import { MemoryRouter } from "react-router-dom";
// Note how with shallow render you search for the React component tag
it("contains 3 NavLinks via shallow", () => {
const numLinks = shallow(<Header />).find("NavLink").length;
expect(numLinks).toEqual(3);
});
// Note how with mount you search for the final rendered HTML since it generates the final DOM.
// We also need to pull in React Router's memoryRouter for testing since the Header expects to have React Router's props passed in.
it("contains 3 anchors via mount", () => {
const numAnchors = mount(
<MemoryRouter>
<Header />
</MemoryRouter>
).find("a").length;
expect(numAnchors).toEqual(3);
});
Production⌗
Its not unusual for the webpacked bundle.js
to weigh in about 2MB.
The development toolchain that has been setup, serves the bundled outputs from memory. Recall that in webpack.config.dev.js
:
module.exports = {
mode: "development",
target: "web",
output: {
// in development mode webpack keeps everything in-memory
path: path.resolve(__dirname, "build"), // the route to serve the in-memory assets
publicPath: "/", // the URI to serve in-memory assets to browser
filename: "bundle.js", // the fake name that can be used to reference the in-memory bundle
},
};
In production, want the webpack bundled assets (index.html
, bundle.js
, styles.css
) dumped out to the /build
folder.
Some goals for a production build:
- lint and run test
- bundle and minify js and css
- create original js and css sourcemaps (to aid debugging)
- shake out development related packages out of the dependency tree
- build react in production mode
- make a bundle report (highlighting dependencies etc)
- deploy the build to a local webserver such as nginx
Redux store config⌗
First the redux store currently has some convenient development related settings such as reduxImmutableStateInvariant
which barfs if you attempt to mutate state. Its a good idea to split out development (configureStore.dev.js
) and production (configureStore.prod.js
) related store configs. In configureStore.js
use CommonJS to dynamically import the appropriate setting based on NODE_ENV
:
if (process.env.NODE_ENV === "production") {
module.exports = require("./configureStore.prod");
} else {
module.exports = require("./configureStore.dev");
}
Webpack⌗
Create a webpack.config.prod.js
in the root of project. Import in MiniCssExtractPlugin
(CSS minifying) and webpackBundleAnalyzer
(webpack analysis report).
Set process.env.NODE_ENV
and mode
to production
.
Change devtool: "cheap-module-source-map"
to devtool: "source-map"
Remove the entire devServer
section.
Using the DefinePlugin
ensure the process.env.NODE_ENV
is defined throughout:
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.API_URL": JSON.stringify("http://localhost:3001"),
}),
Register the webpackBundleAnalyzer
and MiniCssExtract
plugins. Webpack will include a content hash in the CSS bundle name, so a new file will be served up as changes to the CSS occur.
plugins: [
new webpackBundleAnalyzer.BundleAnalyzerPlugin({ analyzerMode: "static" }),
new MiniCssExtractPlugin({
filename: "[name].[contenthash].css",
}),
Update the HtmlWebpackPlugin
plugin which is responsible for generating the base index.html
file and including the necessary includes into it, such as the bundle references. Enable minification of everything, including any html content.
Update the CSS handling to support minification by defining an optimization
section. The OptimizeCSSAssetsPlugin
uses cssnano
under the hood, followed by css-loader
to pull in any css references made throughout the js.
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},
module: {
// what files should webpack handle?
rules: [
{
test: /(\.css)$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
Setup npm scripts⌗
In package.json
define some scripts, with pre and post hooks, to:
- run all unit tests (
test:ci
) - clean the build directory (
rimraf
is a cross platformrm -rf
) - webpack based on production config (cleaning and running unit tests before, and spooling up the fake API and serving the freshly bundled files in a small web server
http-server
)
"scripts": {
"start": "run-p start:dev start:api",
"start:dev": "webpack-dev-server --config webpack.config.dev.js --port 3000",
"prestart:api": "node tools/createMockDb.js",
"start:api": "node tools/apiServer.js",
"test": "jest --watch",
"test:ci": "jest",
"clean:build": "rimraf ./build && mkdir build",
"prebuild": "run-p test:ci clean:build",
"build": "webpack --config webpack.config.prod.js",
"postbuild": "run-p start:api serve:build",
"serve:build": "http-server ./build"
},
Troubleshooting⌗
- Run
npm install
- Don’t use symlinks, as it causes issues with file watches.
- Delete any
.xeslintrc
in your home directory and disable any ESLint plugin. - On Windows? Run shell as an administrator.
- Ensure you do not have
NODE_ENV=production
in your env variables as it will not install thedevDependencies
. - Nothing above work? Delete your
node_modules
folder and re-run npm install.