Power-up React

Redux side-effectsGoing for the best experience

What is a thunk?

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

Thunks in Redux - setup and basic usage

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.

Multiple and asynchronous actions

/* 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

Working with websockets

Setting up the store

/** 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

Receiving actions from the server

/** 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.

Dispatching actions to the 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

Tasks

Load the LISTS from the server
Push task updates to the server
Handle list management: adding and removing lists
Subscribe to task updates via websockets
Subscribe to list updates via websockets
Add authentication
Display a toast (notification) every time a list or task is updated

Redux best practisesTools for the best experience

Setup react-router with Redux

/* 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

Use the Redux router

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

Handling denormalized data

Denormalized state

[
    {
      "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

Normalization schema

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.

Normalized state

{
  "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.

Using normalizr with redux

/* 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.

Immutable data structures

Basics of immutable structures

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.

Using immutable data with redux

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.

Tasks

Replace `react-router` with the Redux version
Normalize the state using `normalizr`
Convert the state to use immutable objects

Redux-sagaTyped Javascript with Flow

Setting up

// 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

are generator functions

This particular saga does nothing but say 'Greetings!' every time an action is dispatched to the store

Start the saga

Handling actions with sagas

/** 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.

Running multiple sagas

/** 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

Tasks

Rewrite the REST API calls to use sagas instead of thunks
Create a saga that handles websocket task update events
Create a saga that handles websocket list update events
Create a saga that handles notifications
Create a saga that handles authentication

PerformanceOptimizing React and Redux for best performance

Measuring the performance

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.

Why did you update?

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!

Common performance anti-patterns

Binding context in callbacks

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

Fixing callbacks

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.

Providing defaults

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.

Avoiding children rerenders

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.

Tasks

Replace `React.Component` with `PureComponent` where appropriate
Remove the performance anti-patterns from the application's code
Optimize selectors using `reselect`
Remove wasted rerenders (`perf-addon`/`why-did-you-update`)

Progressive Web Apps with React

Lighthouse

Install the lighthouse chrome addon

Any page can be inspected using extension

lighthouse is using the Google's PWA checklist

Creating the Service Worker

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

Setting up the cache

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.

Add to home screen

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

Tasks

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

Using types with ReactTyped Javascript with Flow

Installing flow

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

Types in flow

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.

Using flow with React

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

Tasks

Annotate Redux reducers and actions with Flow
Add Flow annotations to a component of your choice
Add Flow annotations to sagas
Annotate all the components

Testing React & Redux

Introducing Jest - the testing lib for React apps

./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.

Testing components

Smoke tests - just checking

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.

Shallow rendering

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

Snapshots

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

Testing Redux

Regular action creators

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

Reducers

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.

Visual testing with Storybook

Setup the storybook

/** 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.

Creating a story

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

Tasks

Unit test a reducer
Create a smoke test for a component of your choice
Use `enzyme` to shallow render and test a component
Setup `storybook` and create a couple of stories for a component
Create a snapshot test of one of the components
Use `enzyme` to test interactions of a component (ie. responding to `onClick`
Use `storybook` to develop a new component

Server-side rendering

Injecting the store with initial state

/** 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.

Creating a NodeJS server

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.

Render the application on server

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.

Run the server

/** 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