Redux and state management

This project is no longer under development.
See the project README for more details.

This page will take you through the steps you need to do to use Redux to manage your application’s state.

Table of contents

General principles

Redux is a small state management container, that is view agnostic and widely used. It is centered around the idea of separating your application logic (the application state) from your view layer, and having the store as a single source of truth for the application state. We recommend reading some of the Redux docs for a good introduction, as well as this awesome cartoon intro to Redux by Lin Clark.

One of the neat features of Redux is that it lets you do time travel debugging; in particular, we use this Chrome extension.

Some definitions

When working with Redux, a bunch of words get used a lot, that might sound confusing at first:

Naming conventions

We recommend structuring your application code as follows:

src
├── store.js
├── actions
│   └── counter.js
│   └── ...
├── reducers
│   └── counter.js
│   └── ...
└── components
    └── simple-counter.js
    └── my-app.js
    └── ...

Connecting elements to the store

What to connect

Generally, anything that needs to have direct access to the store data should be considered a connected element. This includes both updating the state (by calling store.dispatch), or consuming the state (by calling store.subscribe). However, if the element only needs to consume store data, it could receive this data via data bindings from a connected parent element. If you think about a shopping cart example: the cart itself needs to be connected to the store, since “what’s in the cart” is part of the application’s state, but the reusable elements that are rendering each item in the cart don’t need to be connected (since they can just receive their data through a data binding).

Since this is a very application specific decision, one way to start looking at it is to try connecting your lazy-loaded elements, and then go up or down one level from there. That might end up looking something like: screen shot 2018-01-25 at 12 22 39 pm

In this example, only my-app and my-view1 are connected. Since a-element is more of a reusable component rather than an application level component, even if it needs to update the application’s data, it will communicate this via a DOM event, like this.

How to connect

If you want to follow along with actual code, we’ve included a basic Redux counter example in pwa-starter-kit.

Creating a store

If you want to create a simple store, that is not lazy loading reducers, then you probably want something like this:

export const store = createStore(
  reducer,
  compose(applyMiddleware(thunk))
);

Note that this still isn’t the most basic store you can have, since it adds the redux-thunk middleware – this allows you to dispatch async actions, which for any medium-complexity app is a requirement. In most cases however, you’re going to be lazy loading routes, and they should lazy load their reducers, so you want a store that can replace its reducers after it’s been initialized, which is why pwa-starter-kit initializes the store with a lazyReducerEnhancer and the redux-thunk:

export const store = createStore(
  state => state,
  compose(lazyReducerEnhancer(combineReducers), applyMiddleware(thunk))
);

You can find more details on the lazyReducerEnhancer in the Lazy Loading section.

Connecting an element to the store

An element that is connected should call store.subscribe in the constructor, and only update its properties in the change listener passed as the first and only argument (if it needs to). We use a mixin (connect-mixin.ts) from pwa-helpers that does all the connection boilerplate for you, and expects you to implement the stateChanged method. Example use:

import { LitElement, html } from 'lit-element';
import { connect } from  '@polymer/pwa-helpers/connect-mixin.js';
import { store } from './store/store.js';

class MyElement extends connect(store)(LitElement) {
  static get properties() { return {
    clicks: { type: Number },
    value: { type: Number }
  }}

  render() {
    return html`...`;
  }

  // If you don't implement this method, you will get a
  // warning in the console.
  stateChanged(state) {
    this.clicks = state.counter.clicks;
    this.value = state.counter.value;
  }
}

Note that stateChanged gets called any time the store updates, not when only the things you care about update. So in the example above, stateChanged could be called multiple times without state.counter.clicks and state.counter.value ever changing. If you’re doing any expensive work in stateChanged, such as transforming the data from the Redux store (with something like Object.values(state.data.someArray)), consider moving that logic into the render() function (which is called only if the properties update), using a selector, or adding some form of dirty checking:

stateChanged(state) {
  if (this.clicks !== state.counter.clicks) {
    this.clicks = state.counter.clicks;
  }
}

Dispatching actions

If an element needs to dispatch an action to update the store, it should call an action creator:

import { increment } from './store/actions/counter.js';

firstUpdated() {
  // Every time the display of the counter updates, save
  // these values in the store
  this.addEventListener('counter-incremented', function() {
    store.dispatch(increment());
  });
}

Action creators say what the system should do, not what it has already done. This action creator could return a synchronous action:

export const increment = () => {
  return {
    type: INCREMENT
  };
};

An asynchronous one,

export const increment = () => (dispatch, getState) => {
  // Do some sort of work.
  const state = getState();
  const howMuch  = state.counter.doubleIncrement ? 2 : 1;
  dispatch({
      type: INCREMENT,
      howMuch,
    });
  }
};

Or dispatch the result of another action creator:

export const increment = () => (dispatch, getState) => {
  // Do some sort of work.
  const state = getState();
  const howMuch = state.counter.doubleIncrement? 2 : 1;
  dispatch(incrementBy(howMuch));
};

Case study walkthrough

The goal of this walkthrough is to demonstrate how to get started with Redux, by explaining how we added 2 of the standard Redux examples in the pwa-starter-kit template app.

Example 1: Counter

The counter example is very simple: we’re going to add a counter custom element (that you can imagine is a reusable, third party element) to my-view2.js. This example is very detailed, and goes through every line of code that needs to change. If you want a higher level example, check out Example 2. The interaction between the elements, the action creators, action, reducers and the store looks something like this: screen shot 2018-01-25 at 12 44 24 pm

counter.element.js

This is a plain element that’s not connected to the Redux store. It has two properties, clicks and value, and 2 buttons that increment or decrement the value (and always increment clicks).

my-view2.js

This element is an app-level element (as opposed to a reusable element), and it’s connected to the store. This means that it will be able to read and update the application’s state – in particular, the value/clicks properties from counter-element. We need to:

<counter-element value="${this._value}" clicks="${this._clicks}"></counter-element>
<div class="circle">${this._value}</div>
import { connect } from '@polymer/pwa-helpers/connect-mixin.js';
class MyView2 extends connect(store)(LitElement) {
...
}
import counter from '../reducers/counter.js';
store.addReducers({
  counter
});
stateChanged(state) {
  this._clicks = state.counter.clicks;
  this._value = state.counter.value;
}
this.addEventListener('counter-incremented', function() {
  store.dispatch(increment());
})

Example 2: Shopping Cart

The shopping cart example is a little more complex. The main view element (my-view3.js) contains <shop-products>, a list of products that you can choose from, and <shop-cart>, the shopping cart. You can select products from the list to add them to the cart; each product has a limited stock, and can run out. You can perform a checkout in the cart, which has a probability of failing (which in real life could fail because of credit card validation, server errors, etc). It looks like this: screen shot 2018-01-25 at 12 50 22 pm

my-view3.js

This is a connected element that displays both the list of products, the cart, and the Checkout button. It is only connected because it needs to display conditional UI, based on whether the cart has any items (i.e. show a Checkout button or not). This could’ve been an unconnected element if the Checkout button belonged to the cart, for example.

shop-products.js

This element gets the list of products from the store by dispatching the getAllProducts action creator. When the store is updated (by fetching the products from a service, for example), its stateChanged method is called, which populates a products object. Finally, this object is used to render the list of products.

shop-cart.js

Similar to shop-products, this element is also connected to the store and observes both the products and cart state. One of the Redux rules is that there should be only one source of truth, and you should not be duplicating data. For this reason, products is the source of truth (that contains all the available items), and cart contains the indexes, and number of, items that have been added to the cart.

Routing

We use a very simple (but flexible) redux-friendly router, that uses the window location and stores it in store. We do this by using the installRouter helper method provided from the pwa-helpers package:

import { installRouter } from '@polymer/pwa-helpers/router.js';
firstUpdated() {
  installRouter((location) => this._locationChanged(location));
}

Patterns

Connecting DOM events to action creators

If you don’t want to connect every element to the store (and you shouldn’t), unconnected elements will have to communicate the need to update the state in the store.

Manually

You can do this manually by firing event. If <child-element> is unconnected but displays and modifies a property foo:

_onIncrement() {
  this.value++;
  this.dispatchEvent(new CustomEvent('counter-incremented');
}
<counter-element on-counter-incremented="${() => store.dispatch(increment())}"

Or in JavaScript,

firstUpdated() {
  this.addEventListener('counter-incremented', function() {
    store.dispatch(increment());
  });
}

Automatically

Alternatively, you can write a helper to automatically convert any Polymer foo-changed property change event into a Redux action. Note that this requires the <child-element>’s properties to be notifying (i.e. have notify: true), which isn’t necessarily true of all third party elements out there. Here’s an example of that.

Reducers: slice reducers

To make your app more modular, you can split the main state object into parts (“slices”) and have smaller “slice reducers” operate on each part (read more about slice reducers). With the lazyReducerEnhancer, your app can lazily add slice reducers as necessary (e.g. add the counter slice reducer when my-view2.js is imported since only my-view2 operates on that part of the state).

src/store.js:

export const store = createStore(
  state => state,
  compose(lazyReducerEnhancer(combineReducers), applyMiddleware(thunk))
);

src/components/my-view2.js:

// This element is connected to the Redux store.
import { store } from '../store.js';

// We are lazy loading its reducer.
import counter from '../reducers/counter.js';
store.addReducers({
  counter
});

Avoid duplicate state

We avoid storing duplicate data in the state by using the Reselect library. For example, the state may contain a list of items, and one of them is the selected item (e.g. based on the URL). Instead of storing the selected item separately, create a selector that computes the selected item:

import { createSelector } from 'reselect';

const locationSelector = state => state.location;
const itemsSelector = state => state.items;

const selectedItemSelector = createSelector(
  locationSelector,
  itemsSelector,
  (location, items) => items[location.pathname.slice(1)]
);

// To get the selected item:
console.log(selectedItemSelector(state));

To see an example of this, check out the cart example’s cart quantity selector or the item selector from the HN sample app. In both examples, the selector is actually defined in a reducer, since it’s being used both on the Redux side, as well as in the view layer.

How to make sure third-party components don’t mutate the state

Most third-party components were not written to be used in an immutable way, and are not connected to the Redux store so you can’t guarantee that they will not try to update the store. For example, paper-input has a value property, that it updates based on internal actions (i.e. you typing, validating, etc). To make sure that elements like this don’t update the store:

Routing

With Redux, you’re basically on your own for routing. However, we have provided a helper router to get you started. Our suggestion is to update the location state based on window.location. That is, whenever a link is clicked (or the user navigates back), an action is dispatched to update the state based on the location. This works well with time-travel debugging - jumping to a previous state doesn’t affect the URL bar or history stack.

Example of installing and using the router:

// ...
import { installRouter } from '@polymer/pwa-helpers/router.js';

class MyApp extends connect(store)(LitElement) {
    // ...
    firstUpdated() {
      installRouter((location) => this._locationChanged(location));

      // The argument passed to installRouter is a callback. If you don't
      // have any other work to do other than dispatching an action, you
      // can also write something like:
      // installRouter((location) => store.dispatch(navigate(location.pathname)));
    }

    _locationChanged(location) {
      // What action creator you dispatch and what part of the location
      // will depend on your app.
      store.dispatch(navigate(location.pathname));

      // Do other work, if needed, like closing drawers, etc.
    }
  }
}

Lazy loading

One of the main aspects of the PRPL pattern is lazy loading your application’s components as they are needed. If one of these lazy-loaded elements is connected to the store, then your app needs to be able to lazy load that element’s reducers as well.

There are many ways in which you can do this. We’ve implemented one of them as a helper, which can be added to the store:

import lazyReducerEnhancer from '@polymer/pwa-helpers/lazy-reducer-enhancer.js';

// Not-lazy loaded reducers that are initially loaded.
import app from './reducers/app.js';

export const store = createStore(
  state => state,
  compose(lazyReducerEnhancer, applyMiddleware(thunk))
);

// Initially loaded reducers.
store.addReducers({
  app
});

In this example, the application will boot up and install the app reducers, but no others. In your lazy-loaded element, to load its reducer, all you need to do is call store.addReducers:

// If this element was lazy loaded, we must also install its reducer
import { someReducer } from './store/reducers/one.js';
import { someOtherReducer } from './store/reducers/two.js';

// Lazy-load the reducer
store.addReducers({someReducer, someOtherReducer});

class MyElement extends ... {

}

Replicating the state for storage

One of the things your app might want to do is save the state of the app in a storage location (like a database, or localStorage. Redux is very useful for this, since you basically just need to install a new reducer to subscribe to the state, that will dump the state into storage.

To do this, we can first create two functions, called saveState and loadState, to read to/from storage:

export const saveState = (state) => {
  let stringifiedState = JSON.stringify(state);
  localStorage.setItem(MY_KEY, stringifiedState);
}
export const loadState = () => {
  let json = localStorage.getItem(MY_KEY) || '{}';
  let state = JSON.parse(json);

  if (state) {
    return state;
  } else {
    return undefined;  // To use the defaults in the reducers
  }
}

Now, in store.js, we basically want to use the result of loadState() as the default state in the store, and call saveState() every time the store updates:

export const store = createStore(
  state => state,
  loadState(),  // If there is local storage data, load it.
  compose(lazyReducerEnhancer(combineReducers), applyMiddleware(thunk))
);

// This subscriber writes to local storage anytime the state updates.
store.subscribe(() => {
  saveState(store.getState());
});

That’s it! If you want to see a demo of this in a project, the Flash-Cards app implements this pattern.