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 historyinstance
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 songs with authors
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 songs 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 users and authors
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 Lists.
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 Maps.
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.
yielding 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
Enjoy!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