function answer() {
return 42;
}
function greeting(name) {
return () => `Hello, ${name}!`;
}
function fetchData(params, transformFn) {
return function() {
API.fetch(params)
.then(result => transformFn(result));
}
}
A thunk is just a function that will return some data or perform an operation when called.
The function takes no arguments - it has access to all data it needs.
Most often the thunk will have access to some params via closure.
The thunk can do anything - synchoronous or not
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
/* reducers, */
applyMiddleware(thunk)
);
function greet(name) {
return { type: 'GREET', name }
}
function hello(name) {
return function (dispatch) {
dispatch(greet('Maciej'));
};
}
Import standard methods from redux
.
The redux-thunk
library provides a middleware
Create a store providing reducers (out of slide) and applying thunk middleware
Very simple action creator
hello
is a function that will return a thunk - it's here for the purpose of creating a closure over name
argument
Thanks to the thunk
middleware, the thunk will actually get the store's dispatch
function as first argument.
Use the dispatch
to dispatch greet
action.
/* imports, setup, middleware */
function timerStart(timerId) {
return { type: 'TIMER_START', timerId };
}
function timerEnd() {
return { type: 'TIMER_END' };
}
function runTimer(delay) {
return function(dispatch) {
const timerId = setTimeout(() => {
dispatch(timerEnd());
}, delay * 1000);
dispatch(timerStart(timerId));
};
}
store.dispatch(runTimer(5));
Imports and setup of store and middleware - just as before
Define regular, synchronous action creators
Create a thunk that takes in one argument
At first it will dispatch the TIMER_START
action
After the delay, dispatch TIMER_END
action
Still dispatches like a regular action
/** CONSOLE **/
npm install --save socket.io
npm install --save redux-socket.io
/** STORE.JS **/
import { createStore, applyMiddleware } from 'redux';
import createSocketIoMiddleware from 'redux-socket.io';
import io from 'socket.io-client';
let socket = io(SOCKET_ADDRESS);
let socketIoMiddleware = createSocketIoMiddleware(socket, "SERVER/");
const store = createStore(
/* reducers, */
applyMiddleware(socketIoMiddleware)
);
Install required packages
Create a socket connection using the socket.io
library
Create the middleware for handling sockets. It takes the socket as first argument.
The second argument is the socket action prefix:
in this example, all actions prefixed with SERVER/
(SERVER/ADD_TODO
, etc) will emit a message through the socket
Standard store setup
/** SERVER **/
socket.emit('action', {
type: 'ADD_PRODUCT',
payload: { /* product */ }
});
/** CLIENT **/
function productsReducer(state = [], action) {
switch (action.type) {
case 'ADD_PRODUCT':
return [...state, action.payload];
default:
return state;
}
}
Server will emit action
events, which data is the action to dispatch to store
On the client-side, subscribing to events is handled by the socket.io middleware
Since an action is just a plain object, there is no difference between handling action dispached on client or server.
/** CLIENT **/
function addProduct(product) {
return {
type: 'SERVER/ADD_PRODUCT',
payload: product
};
}
store.dispatch(addProduct({ /* product */ }));
/** SERVER **/
socket.on('action', (action) => {
switch (action.type) {
case 'SERVER/ADD_PRODUCT':
/* handle adding product */
}
});
Action that will be dispatched to the server via websockets is just a regular action.
The only difference: the action type starts with SERVER/
, which matches the middleware configuration
Dispatch the action as usual
The server will receive action
event, with the action as payload
The action will be handled depending on its type
/* INSTALLATION */
// npm install --save react-router-redux@next
// npm install --save history
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { routerReducer, routerMiddleware } from 'react-router-redux';
import createBrowserHistory from 'history/createBrowserHistory';
export const history = createBrowserHistory();
const middleware = routerMiddleware(history);
export const store = createStore(
combineReducers({
/* ...reducers, */
router: routerReducer
}),
applyMiddleware(middleware)
)
Install the required packages
Remember to add the @next
suffix -
the default version of react-router-redux
works only with old react-router
versions
history
is a library that allows easy management of session history via JS.
The createBrowserHistory
method is using HTML5 History API and should be used with new browsers.
Create and export a history
instance for the application
Setup routing middleware to use the history
instance
Standard store setup with multiple reducers and a middleware
Routing state is in the router
sub-tree of the application state
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux';
import { Route } from 'react-router';
import { ConnectedRouter } from 'react-router-redux';
import {history, store} from "./store"
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</div>
</ConnectedRouter>
</Provider>,
document.getElementById('root')
)
Standard imports for the router component
Instead of Router
or BrowserRouter
import the ConnectedRouter
Standard Redux setup
The <ConnectedRouter />
component should be a child of the <Provider />
so that it knows to use the application's store
Setting up <Route />
's is the same as before
[
{
"id": 0,
"user": { "id": 0, "name": "Maciej" },
"songs": [
{
"id": 1,
"title": "Far Behind",
"author": { "id": 200, "name": "Oleska" }
},
{
"id": 2,
"title": "Celebration",
"author": { "id": 200, "name": "Oleska" }
},
{
"id": 3,
"title": "Back to Back",
"author": { "id": 205, "name": "Drake" }
}
]
},
{
"id": 1,
"user": { "id": 1, "name": "Wojtek" },
"songs": [
{
"id": 1,
"title": "Far Behind",
"author": { "id": 200, "name": "Oleska" }
},
{
"id": 4,
"title": "Tuesday",
"author": { "id": 205, "name": "Drake" }
}
]
}
]
The following JSON is a result of calling GET /playlists
The API returns an array of playlists
,
each containing array of song
s with author
s
and the user
the playlist
belongs to.
This is a standard way of dealing with relations in data
and appears very often (ie. posts
-comments
-authors
)
Problem #1: duplicated data - some songs and authors appear more than once
Problem #2: very difficult to update nested data and remain consistent
playlistsNormalizer.js// npm install --save normalizr
const { normalize, schema } = require('normalizr');
const userSchema = new schema.Entity('users');
const authorSchema = new schema.Entity('authors');
export const songSchema = new schema.Entity('songs', {
author: authorSchema
});
export const playlistSchema = new schema.Entity('playlists', {
user: userSchema,
songs: [songSchema]
});
export const playlistsSchema = [playlistSchema];
export default function(data) {
return normalize(data, playlistsSchema);
}
Install normalizr
A playlist
contains a user
and song
s with authors
. In normalizr those are called entities.
All entities are instances of the Entity
class.
Entities with no relationships are the easiest to cover.
Each song
contains an author
.
Each playlist
has a user
and a list of songs
Final schema is a list of playlists
.
The normalize
function takes some denormalized
data
and normalizes it using the provided schema
.
{
"entities": {
"result": [ 0, 1 ],
"playlists": {
"0": {
"id": 0,
"user": 0,
"songs": [ 1, 2, 3 ]
},
"1": {
"id": 1,
"user": 1,
"songs": [ 1, 4 ]
}
},
"songs": {
"1": {
"id": 1,
"title": "Far Behind",
"author": 200
},
"2": {
"id": 2,
"title": "Celebration",
"author": 200
},
"3": {
"id": 3,
"title": "Back to Back",
"author": 205
},
"4": {
"id": 4,
"title": "Tuesday",
"author": 205
}
},
"users": {
"0": { "id": 0, "name": "Maciej" },
"1": { "id": 1, "name": "Wojtek" }
},
"authors": {
"200": { "id": 200, "name": "Oleska" },
"205": { "id": 205, "name": "Drake" }
}
}
}
The following object is a result using normalizr
on the list of playlists
A playlist
now doesn't hold any data belonding to another entity - just the ids.
Each song
is represented exactly once.
Same goes for user
s and author
s
Entities are records in a dictionary which keys are the entity ids.
Since playlists changed from an array to an object the ordering was lost.
The result
field holds the original order of elements.
/* REDUCER */
import normalizePlaylists from './schema';
function reducer(state = {}, action) {
switch (action.type) {
case 'DESERIALIZE_PLAYLISTS':
const normalized = normalizePlaylists(action.payload);
return Object.assign({}, state, normalized.entities);
default:
return state;
}
}
/* CONTAINER */
import {denormalize} from 'normalizr';
import {playlistSchema} from './schema'
function mapStateToProps(state) {
const normalizedPlaylist = state.playlists[state.currentPlaylistId];
const denormalizedPlaylist = denormalize(
normalizedPlaylist,
playlistSchema,
state
);
return {
currentPlaylist: denormalizedPlaylist
};
}
export default connect(mapStateToProps)(Container)
To keep the state normalized all updates should normalize the data first
The normalizePlaylists
is a function exported from the schema definition file (one of previous slides)
action.payload
is the denormalized API response
Update the application state with fully normalized entities
The normalized state, while easy to manage, is not useful to components and requries some transformation
Get currently viewed playlist. The data is normalized and it's denormalization quite complicated - there are multiple levels of nesting.
Use the denormalize
utility from normalizr
,
passing in normalized data, the normalization schema,
and all relevant entities.
The playlistSchema
is defined on earlier slide.
import {Map, List, fromJS} from 'immutable';
const mutableCollection = [1, 2, 3];
mutableCollection.push(4);
// mutableCollection => [1, 2, 3, 4]
const immutableCollection = List([1, 2, 3]);
immutableCollection.push(4);
// immutableCollection => [1, 2, 3]
const mutableObject = { foo: 1, bar: 2 };
console.log(mutableObject.foo); // 1
mutableObject.bar = 'baz';
// mutableObject => { foo: 1, bar: 'baz' }
const immutableObject = Map({ foo: 1, bar: 2});
immutableObject.get('foo'); // 1
immutableObject.set('bar', 'baz');
// immutableObject => { foo: 1, bar: 2 }
const immutableStructure = fromJS({ /* properties */ });
const mutableAgain = immutableStructure.toJS();
Mutable collection (aka regular array) can be changed with methods or overiding the data
Immutable collections are called List
s.
Pushing to a List
will return a new instance.
Original List
will be unchanged.
Same principle applies to objects. Plain JS objects can be easily modified.
Immutable objects are called Map
s.
They are less straight-forward to access (.get
), but will return a new object after set
.
Converting deeply nesting structures to Immutable is easy with fromJS
utility
Each of Immutablejs structures has a toJS
method that allows to convert the structure back to plan JS.
import {Map, fromJS} from 'immutable';
const defaultState = Map();
function reducer(state = defaultState, action) {
switch (action.type) {
case 'UPDATE_STATE':
return state.merge(action.payload);
case 'ADD_ITEM':
const items = state.get('items')
.push(Map(action.payload));
return state.merge({ items });
default:
return state;
}
}
Define the defaultState
for this reducer as an immutable object
General state updates can be handled with merge
or mergeDeep
utilities
Actions that create more specific updates need custom logic.
Never set
or push
mutable object to otherwise immutable structure.
Regular objects need to be converted to immutable with proper constructor function.
When in doubt use fromJS
. merge
and mergeDeep
are safe too.
// npm install --save redux-saga
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
/* reducer, */
applyMiddleware(sagaMiddleware)
);
function* greeterSaga() {
console.log('Greetings!')
}
sagaMiddleware.run(greeterSaga);
Install the redux-saga
package
Import and create the middlewares for handling sagas
Create the store as usual, passing in the middleware
This particular saga does nothing but say 'Greetings!' every time an action is dispatched to the store
Start the saga
/** ACTION CREATORS **/
function timerStart(time) {
return { type: 'TIMER_START', time };
}
function timerEnd() {
return { type: 'TIMER_END' };
}
/** SAGAS **/
import { delay } from 'redux-saga';
import { put, takeEvery } from 'redux-saga/effects';
function* timerSaga() {
yield takeEvery('TIMER_START', runTimer);
}
function* runTimer(action) {
yield delay(action.time * 1000);
yield put(timerEnd())
}
/** STORE **/
sagaMiddleware.run(timerSaga);
store.dispatch(timerStart(30));
Let's rewrite the thunk timer example to sagas
The action creators remained almost unchanged;
the runTimer
action creator was removed
Import utilities from the redux-saga
lib
The timerSaga
watches for TIMER_START
actions using the takeEvery
effect.
When such action is dispached it will call the runTimer
saga with the action.
The runTimer
saga is tasked with handling individual actions
It receives the action
object as an argument
The delay
effect returns a promise that will be resolved after N miliseconds.
yield
ing the promise to saga's middleware will cause the execution of runTimer
to pause until the promise is resolved.
After the scheduled delay, runTimer
will put
the TIMER_END
action.
Using put
will dispatch the action that was passed in
Use the saga middleware to run the timerSaga
.
/** SAGAS/INDEX.JS **/
import { all } from `redux-saga/effects`;
import {sagaA} from './sagaA';
import {sagaB} from './sagaB';
/* ... */
export function* rootSaga() {
yield all([
sagaA(),
sagaB(),
/* ... */
])
}
/** STORE.JS **/
import {rootSaga} from "./sagas"
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
/* reducer, */
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);
The .run
method on saga middleware can only run a single saga
Import all required sagas
Create a top-level saga
Run all sagas in parallel using the all
utility.
Run the top-level saga on the middleware
store.js// npm install --save-dev react-addons-perf
if (process.env.NODE_ENV !== 'production') {
window.Perf = require('react-addons-perf');
}
/* ... */
/* BROWSER CONSOLE */
> Perf.start()
/* ... */
> Perf.stop();
> Perf.getLastMeasurements();
> Perf.printExclusive();
> Perf.printWasted();
Install the react-addons-perf
package
Import and assign to the window
object to make it available
via console in the browser
Proceed with normal React setup
All following commands will be issued in the browser's console
Start measuring performance
Use the app
Stop the measurement
Get the total render count/time for every component. Useful to get an overview on which components are slow.
More detailed view of the same measurements. Doesn't include the time of mounting components.
Shows renders that happened (the render
function was called)
but resulted in no changes in DOM.
index.js// npm install --save-dev why-did-you-update
import React from 'react'
if (process.env.NODE_ENV !== 'production') {
const {whyDidYouUpdate} = require('why-did-you-update');
whyDidYouUpdate(React);
}
Install the why-did-you-update
package
Import the setup function
Start reporting unnecessary rerenders
class ComponentA extends React.Component {
handleUserAction(e) {
// ...
}
render() {
return (
<button onClick={this.handleUserAction.bind(this)}>
Click
</button>
);
}
}
class ComponentB extends React.Component {
handleUserAction() {
// ...
}
render() {
return (
<button onClick={(e) => this.handleUserAction(e)}>
Click
</button>
);
}
}
ComponentA
wants to pass a callback to its children.
The callback uses this
to reference the current ComponentA
instance.
Common solution: bind the function context in the component's render.
Antipattern: the children component tree (or DOM) needs to be rerendered,
because .bind
returns a new function every time.
ComponentB
attempts to solve the same issue
Instead of using .bind
it uses an arrow function to keep the context.
Same issue as before: new function created with every render
class ComponentA extends React.Component {
constructor(props) {
super(props);
this.handleUserAction = this.handleUserAction.bind(this);
}
render() {
return (
<button onClick={this.handleUserAction}>
Click
</button>
);
}
}
class ComponentB extends React.Component {
handleUserAction = () => {
// ...
}
render() {
return (
<button onClick={this.handleUserAction}>
Click
</button>
);
}
}
ComponentA
was rewritten to avoid the anti-pattern.
The callback is bound to the component's instance in the constructor.
this.handleUserAction
references the same function every time the ComponentA
renders.
React will have no trouble to recognize that there was no change; no rerendering of children will happen
ComponentB
solves the issue in more modern way
Instead of using .bind
in constructor define the method as an arrow function.
Important! Class properties will not work in pure ES2015+ right now.
It's a stage-2
proposal to the language.
Enabled by default in create-react-app
.
class BadComponent extends React.Component {
render() {
const options = this.props.options || {};
return (
<AnotherComponent options={options} />
);
}
}
class GoodComponent extends React.Component {
defaultOptions = {};
render() {
const options = this.props.options || this.defaultOptions;
return (
<AnotherComponent options={options} />
);
}
}
class AlsoGoodComponent extends React.Component {
static defaultProps = {
options: {}
};
render() {
const options = this.props.options;
return (
<AnotherComponent options={options} />
);
}
}
BadComponent
provides some options to it's child component.
In case it didn't recieve the options as props, the empty object is provided as default.
Anitpattern: Much like with callbacks, AnotherComponent
will be rendered every time.
Solution: the default options must be shared between rerenders
GoodComponent
uses a class property to make sure that defaultOptions
is the same object everytime.
AlsoGoodComponent
uses the React's defaultProps
feature.
class BadComponent extends React.Component {
/* ... */
render() {
return (
<FastComponent /*props*/>
<ChildComponent />
</FastComponent>
);
}
}
class GoodComponent extends React.Component {
childComponent = <ChildComponent />;
render() {
return (
<FastComponent /*props*/>
{this.childComponent}
</FastComponent>
);
}
}
BadComponent
renders a well-optimized FastComponent
and provides it's child: the ChildComponent
.
JSX is just syntax-sugar over React.createElement
which returns the node.
Calling the function returns a new object every time.
Nested elements are passed as children
prop to the component.
Antipattern: FastComponent
will rerender, because one of it's props (children
) is different with every BadComponent
render.
Solution: cache the ChildComponent
in a class property.
Install the lighthouse
chrome addon
Any page can be inspected using extension
lighthouse
is using the Google's PWA checklist
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker
.register('service-worker.js')
.then(function (registration) {
// registration successful
}).catch(function (err) {
console.log('ServiceWorker registration failed: ', err)
});
});
}
The following code should run as the application loads.
Good place for it might be the very bottom of index.html
file.
Only run the code in browsers that actually support service workers
After the app fully loaded...
... register the service worker that is located in public/service-worker.js
.
Log an error if there was a problem while registering the service worker
public/service-worker.jsconst CACHE_NAME = 'react-workshop-v1';
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
fetch("asset-manifest.json")
.then(res => res.json())
.then(assets => {
const urlsToCache = [
"/",
assets["main.js"],
assets["main.css"]
];
cache.addAll(urlsToCache);
});
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
The name identifies cache of the application and allows easy invalidation by simply changing the name
When the user starts the application the install
event will be triggered for the service worker.
Load the application cache object
Problem: in production environment webpack
will add a hash to names of JS/CSS bundles
It will also produce the asset-manifest.json
file, which maps the default filenames to hashed ones.
List of files to add to cache. /
refers to the index.html
.
Additional files, like fonts or images, can also be cached.
fetch
event is triggered whenever the browser requests a resource.
Lookup the request in cache. If the response is there, return it immediately. Otherwise, proceed with fetch.
public/manifest.json{
"short_name": "React Workshop",
"name": "Devmeetings' React Workshop",
"icons": [
{
"src":"icon.png",
"sizes": "192x192",
"type": "image/png"
}
],
"start_url": "/",
"background_color": "#fff",
"theme_color": "#fff",
"display": "standalone"
}
The manifest.json
file describes how the application should be displayed
when added to phone's home screen
The name to display below the icon
The name to display on the splash screen while the app is starting up
The icon to display
URL that the app should open on. In this case, root url is just fine.
The background color of the splashscreen. Ideally should match application background.
Color of the chrome and navigation elements
Display mode for the application: standalone
stands for native-live look
Install lighthouse
and inspect the page
Register the service worker if to browser supports this feature
Setup caching by handling the install
and fetch
events
Create a placeholder page to show before the app loads
Add the "Add to home screen" capability
Max out the lighthouse score
Add offline-first features using redux-offline
console/** CONSOLE **/
npm install --save-dev flow-bin
/** PACKAGE.JSON **/
{
/* ... */
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"flow": "flow"
}
}
/** CONSOLE **/
npm run flow init
npm start
npm run flow
Install the flow-bin
package with npm
or yarn
Add the flow
script to the scripts
section of package.json
. Not necessary if installing via yarn
.
Run the initialization script with flow init
.
Start the app normally, the create-react-app
will check the types automatically.
Running the flow
script will also check types
console/* @flow */
const bar: number = 2;
function foo(x: ?string): string {
if (x) {
return x;
}
return "default string";
}
const arrayOfNumbers: number[] = [1, 2, 3];
interface Circle {
position: ?{
x: number,
y: number
}
radius: number;
area(): number;
}
const circle: Circle = {
radius: 2,
area() {
return Math.PI * this.radius * this.radius;
}
};
class Foo {
bar() {}
}
const fooInstance: Foo = new Foo();
Mark the file as flow-typed with the @flow
directive
Declare variable types with the variableName: type
syntax
Function's arguments can be typed too
Optional arguments are marked with the ?
character prefixing the type
Function's return type is defined after the argument list
Array types are defined using the Type[]
syntax
Custom types can be defined using interfaces
.
Interfaces exists only for typing purposes and do not appear in compiled code.
Complex types can also be defined inline.
Interfaces can define typing for methods as well.
Interface can be used in the same way as any other type
Another way to define a type is to use a class
All instances of class Foo
are of type Foo
.
console/* @flow */
const bar: number = 2;
function foo(x: ?string): string {
if (x) {
return x;
}
return "default string";
}
const arrayOfNumbers: number[] = [1, 2, 3];
interface Circle {
position: ?{
x: number,
y: number
}
radius: number;
area(): number;
}
const circle: Circle = {
radius: 2,
area() {
return Math.PI * this.radius * this.radius;
}
};
class Foo {
bar() {}
}
const fooInstance: Foo = new Foo();
The @flow
directive is necessary in every file
state
and props
are important properties to type for a React component
Create an interface for the state
.
The +
prefix means that the property is read-only.
The state should be considered read-only, as it is only changed via setState
.
Same goes for the props
Provide type and initial value for the state
.
Provide type for the props
Flow will recognize that properties in defaultProps
should be considered optional
./services/Service.test.jsclass Service {
foo = 6;
bar(value) {
return Boolean(value);
}
}
describe('Service', () => {
let instance;
beforeEach(() => {
instance = new Service();
});
it('has `foo` property', () => {
expect(instance.foo).toBe(6);
});
describe('#bar', () => {
it('returns true if passed truthy value', () => {
expect(instance.bar(true)).toBe(true);
});
});
});
To properly work with the default create-react-app
config,
all tests are in files which names end with .test.js
.
For example, if MyComponent
is in file MyComponent.js
,
it's tests should be in file called MyComponent.test.js
Tests are grouped in describe
blocks.
The top level describe
is actually optional:
properly naming the file should be enough,
provided there is only one class/service/component per file
Code in beforeEach
block will be run before each test.
This block should be used for initialization of fixtures/common setup.
There should be no shared state between tests.
A test is a function (it
or test
) that takes 2 arguments:
the test description
and a function that will contain expectations
The expect
function is used for testing values.
It is followed by a matcher -- a function that asserts something about the value.
In this case it's toBe
which checks equality
describe
blocks can be nested, along with their own beforeEach
/afterEach
functions.
Nested describes
are using for grouping tests.
MyComponent.test.jsimport React from 'react';
import ReactDOM from 'react-dom';
import MyComponent from './MyComponent';
describe('<MyComponent/>', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<MyComponent />, div);
});
})
By far the simplest test of React component test is a smoke test: just seeing if there are no errors when using the component. It's often enough for functional/presentational components that have no logic
Import required React packages
Import the component
Try to render the component into a div
This test has no expect
statements. It will pass unless an error is thrown.
Note: this is not exactly unit testing: if there are no errors, then entire component subtree will be rendered.
MyComponent.test.jsimport React from 'react';
import { shallow } from 'enzyme';
import MyComponent from './MyComponent';
describe('<MyComponent/>', () => {
it('renders the button', () => {
const wrapper = shallow(<MyComponent>Hello</MyComponent>);
expect(wrapper.contains(<button>Hello</button>)).toBe(true);
});
})
Enzyme
lib, especially the shallow
renderer are used for proper unit-testing React components
Instead of the standard ReactDOM
, the rendering will be handled by shallow
renderer from enzyme
Use it to render <MyComponent />
The component will not try to render any of other React components that are children to <MyComponent/>
.
They will be treat as normal HTML elements instead.
wrapper
is a smart wrapper over a React node, that exposes various API to query and interact with the subtree
MyComponent.test.jsimport React from 'react';
import renderer from 'react-test-renderer';
import MyComponent from './MyComponent';
it('renders correctly', () => {
const tree = renderer.create(
<MyComponent message="Hello!">
Click here!
</MyComponent>
).toJSON();
expect(tree).toMatchSnapshot();
});
/** SNAPSHOT **/
exports[`renders correctly 1`] = `
<div className="my-component">
<button onClick={[Function]}>Click here!</button>
</div>
`;
/** CHANGED TEST **/
it('renders correctly', () => {
const tree = renderer.create(
<MyComponent message="Hello!">
And now here!
</MyComponent>
).toJSON();
expect(tree).toMatchSnapshot();
});
/** CONSOLE **/
jest --updateSnapshot
Snapshots are used to prevent accidental changes of UI
Import renderer
that will render the component instead of the usual ReactDOM
Use it to render <MyComponent />
The renderer.create
method returns a JS object representing the rendered component
Use the toMatchSnapshot
to check if generated tree matches the previously saved snapshot
This test will pass the first time it's run, because there is no snapshot to match against. At the same time the snapshot will be created.
The snapshot is JSX-like representation of the rendered subtree.
Snapshots are saved to a file with .snap
extension that should be checked into the VCS
If the test or the component are modified, matching against the snapshot will fail
Replace old snapshots with new version
MyComponent.test.js/** ACTION CREATOR **/
export function addTodo(text) {
return {
type: 'ADD_TODO',
payload: {
text,
completed: false
}
};
}
/** TEST **/
describe('addTodo', () => {
it('returns action with correct type', () => {
const action = addTodo('Learn React');
expect(action.type).toBe('ADD_TODO');
});
it('returns action with the todo in payload', () => {
const action = addTodo('Learn React');
expect(action.payload.text).toBe('Learn React');
});
});
Synchronous action creators are usually very simple functions, so testing is staright-forward
An action creator straight from the world's most popular React application
Check for correct type
Check for correct payload
Such simple action creators are commonly tested indirectly when testing reducers and have no tests on their own
MyComponent.test.jsimport todos from 'reducers/todos';
describe('todos reducer', () => {
it('handles ADD_TODO', () => {
const state = todos([], addTodo('Test AddTodo'));
const expected = [
{
text: 'Test AddTodo',
completed: false
}
];
expect(state).toEqual(expected);
});
});
Reducers are pure functions, which makes them very easy to test
Import the reducer
The reducer takes current state
and an action
and returns new state
.
Normally this bit would be called by Redux.
Define the expected shape of new state
.
Perform deep equality check with toEqual
matcher.
/** CONSOLE **/
npm i -g @storybook/cli
/** IN PROJECT DIRECTORY **/
getstorybook
npm run storybook
Install the storybook
command line tool
In the main project directory run the getstorybook
command,
which will setup the storybook for current application
Start the storybook app. Go to http://localhost:9009/
to see the example storybook.
The storybook loads stories defined in src/stories/index.js
or src/stories.js
files.
Delete to file contents to remove the default stories.
MyComponent.stories.js/** CONSOLE **/
npm i -g @storybook/cli
/** IN PROJECT DIRECTORY **/
getstorybook
npm run storybook
By convention, the stories for given component are defined in the same directory in file called COMPONENT_NAME.stories.js
.
Required imports
Create a group of stories; should be called the same as the component under test
Add a story with name and a function that returns the component with certain params
Add more stories with different params
Remember to import component stories into the main stories file
/** STORE **/
export function configureStore(initialState = {}) {
const store = createStore(
rootReducer,
initialState,
/* middlewares */
);
return store;
}
/** APP **/
// import {store} from '../store';
import {configureStore} from '../store';
const store = configureStore(/* initial state */);
class App extends React.Component {
render() {
return (
<Provider store={store}>
/* application code */
</Provider>
);
}
}
Instead of creating the store immediately, export the configureStore
function.
This will enable the app to dynamically inject the initial state if needed.
Store is created in the main app file
Rest of the app runs as normal.
ssr/index.js// yarn add ignore-styles babel-register express
require('ignore-styles');
require('babel-register')({
ignore: /\/(build|node_modules)\//,
presets: ['env', 'react-app']
});
const express = require('express');
const path = require('path');
const app = express();
app.get('/', require('./render'));
app.use(express.static(path.resolve(__dirname, '..', 'build')));
app.use('/', require('./render'));
const PORT = process.env.PORT || 8080;
app.listen(PORT, ()=>{
console.log(`App listening on port ${PORT}!`)
});
Install required packages
NodeJS doesn't fully support modern Javascript; in particular imports
are not supported.
'babel-require' is used to avoid this problem and also enable JSX on the server.
All requests to the /
url will render
the application.
Details of the render
handler will be on the next slide.
All static files will be served from the build
directory.
All other request will also use the render
handler.
ssr/index.jsconst path = require('path');
const fs = require('fs');
const React = require('react');
const {Provider} = require('react-redux');
const {renderToString} = require('react-dom/server');
const {StaticRouter, Route} = require('react-router-dom');
const {configureStore} = require('../src/store/index');
const {default: Board} = require('../src/components/Board/Board');
const {default: CardDetails} = require('../src/components/CardDetails/CardDetails');
module.exports = function renderApp(req, res) {
const filePath = path.resolve(__dirname, '..', 'build', 'index.html');
/* get the INITIAL_STATE) */
fs.readFile(filePath, 'utf8', (err, indexHtml) => {
const store = configureStore(INITIAL_STATE);
const markup = renderToString(
<Provider store={store}>
<StaticRouter location={req.url}>
<div>
<Route component={Board}/>
<Route path="/details/:id" component={CardDetails} />
</div>
</StaticRouter>
</Provider>
);
const App = indexHtml.replace('{{SSR}}', markup);
res.send(App);
});
};
Use require
to import React and React-related packages
While client uses the standard ReactDOM.render
, the server will use the renderToString
method from react-dom/server
;
Similarly, the routing will be handled by StaticRouter
instead of the BrowserRouter
.
Import the configureStore
and components requried for routing.
Get the build/index.html
file, which is a compiled version of public/index.html
file.
Query the database, load files, etc, to create an object representing the initial state of the app.
Create a React markup that will handle the routing on the client-side
Inject the markup into the html and send it to client.
/** PUBLIC/INDEX.HTML **/
// before
<div id="root"></div>
// now
<div id="root">{{SSR}}</div>
/** CONSOLE **/
npm run build
NODE_ENV=production node ssr
The {{SSR}}
token is added to the public/index.html
file
to mark the place for server to render the application.
The build
command uses webpack
to prepare a static bundle
Run the server. This assumes that code for the server-side rendering is in the ssr/index.js