- 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
React
React is a JavaScript library for building dynamic and interactive user interfaces.
As React is simply a library and not a fully featured framework like Angular or Vue, it is common to need additional tools to deal with concerns such as routing, state management, internationalization, form validation, etc.
React popularised representing markup as JSX (JavaScript XML), a syntax that combines HTML and JavaScript in an expressive way, making it easier to create complex user interfaces.
At runtime React takes a tree of components and builds a JavaScript data structure called the virtual DOM. This virtual DOM is different from the actual DOM in the browser. It’s an efficient, in-memory representation of the component tree. When the state or the data of a component changes, React updates the corresponding nodes in the virtual DOM and compares it against the previous version to identify elements that need updating in the real DOM.
Development environment
- Install Node with nvm
- 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 - formatter
- ESLint - linter to get red swigglys under suspect 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-eslintparser provides strong compatibility with babel generated JavaScript, including support for ES2018, ESM (EcmaScript Modules) and JSX. envtells ESLint to ignore use of global variables for well known packages that you intend to use.rulesrelaxes the strictness around useful things likeconsole.loganddebuggerrootmakes 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.
handleDeleteresponsible 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 ReactssetStatefunction (see below)pass this handler function downstream to
Countercomponents aspropsclass 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
propsfrom its parent. - Never manages this as
statelocally, but instead fires events to request the data be updated back in the parent.
Refactoring steps:
- Remove any local
state - Any references to
this.stateshould be updated to usepropsdirectly, 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:
constructorcomponent constructorrenderthe component itself, and all its childrencomponentDidMountafter 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:
rendercomponentDidUpdatecalled 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).
componentWillUnmountfired 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.
componentDidMountis 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:
BrowserRouterHashRouterLinkprovides declarative navigation around your application. Note the use of backticks for setting thetoprop 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 Routes:
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 (
rimrafis 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
.xeslintrcin your home directory and disable any ESLint plugin. - On Windows? Run shell as an administrator.
- Ensure you do not have
NODE_ENV=productionin your env variables as it will not install thedevDependencies. - Nothing above work? Delete your
node_modulesfolder and re-run npm install.