---
title: Redux for Chrome Extensions
teaser: An intro to functional state management for your Chrome extensions using Redux.
tags: web,javascript,chrome,redux
author: Bruno Antunes
published_on: 2017-10-02
---

[React] took the frontend development scene by storm a long time ago.
It's great, and made a JavaScript fan out of me. However, being just a view
 layer and all, it usually takes a few more parts to reach a  complete solution.

If you find yourself overwhelmed with property-passing down components trees and
starting to lose your grip on event handlers, then [Redux], a "predictable state
container for JavaScript apps", might prove valuable for your stack.

## Redux in a nutshell

A fullblown intro to Redux is a bit out of scope for this blog post, so let's
 just go over the basics. To learn more, check out the [official docs] or
 LearnCode.academy's excellent [playlist on Redux] which goes over React
 integration as well.

Redux is completely independent from React. In fact, React is not even required.
Like [Flux], there's an application-wide **Store** object that holds all of your
state. Unlike Flux, Redux uses a single global store and not one per model or
concern.

**Actions** are triggered from your UI components and run through one or more
functions called **reducers**. Example action<sup>[1](#fsa)</sup>:

```js
{
  type: 'LISTS_REFRESH_FULFILLED',
  payload: {
    data: [
      {icon: 'rss', name: 'RSS', id: 1},
    ]
  }
}
```

Redux stores expose [four functions], and actions such as the one in the example
above are dispatched using the store's aptly-named `dispatch()` function.
After dispatching the action from the store, your reducers take the current
state and the dispatched action, and return a new state. Here's an example of a
 reducer:

```js
function listsReducer(state, action) {
  switch(action.type) {
    case 'LISTS_REFRESH_START':
      break

    case 'LISTS_REFRESH_FULFILLED':
      const records = action.payload.data.map( (item) => {
        return {
          icon: item.icon,
          name: item.name,
          id: item.id,
        }
      })
      state =  {...state, records: records, activeId: records[0].id}
      break
  }

  return state
}
```

Once the state update is finalized in the **Store**, React re-renders your
components, and the cycle can begin anew.

![Redux concept flow](https://images.thoughtbot.com/blog-vellum-image-uploads/OSHyw7ebQcq3Zq1lzBAM_28973675-df893164-792b-11e7-8c80-ab0f82921974.png)

## Redux and Chrome extensions

We've written about [how to write a Chrome extension] on the blog, but again
let's go a bit deeper into background and popup pages, and how Redux can help
organize your extension's code.

A Chrome extension is composed of a manifest file, one or more HTML files
(unless it is a [theme extension]), and optionally supporting files
(JavaScript, CSS, images, etc.). Extensions can also add a button to the
browser's toolbar, and when clicked this button may display a [popup] under it.
These popups are not long-lived, since they are created once needed and disposed
of by the browser when closed. For long-lived scripts, you need a
[background script] to be declared on your manifest.

The small example I'll be showing on this post is for a bookmark saver extension
which shows a popup allowing you to decide onto which remote list you want to
save the current URL. A persistent background script will hold state in a Redux
store, and the popup will sync its state with the background script's store when
clicked.

JavaScript running on a popup page can't directly communicate with the
background script, since it runs on a different context. To communicate with a
persistent background script, we make use of [message passing]. This serves as
an apt analogy of the action dispatching nature of Redux itself. A very useful
NPM module that I'll be using for this example is `react-chrome-redux`, which
wraps the Chrome message passing API onto Redux mechanics. Its [overview] goes
over the concept.

## Getting started

Let's start with the `package.json` file. You can also install the listed
dependencies manually, if you prefer.

```js
{
  "name": "bookmark-saver",
  "description": "Redux Chrome extension example",
  "repository": {
    "type": "git",
    "url": "git@github.com:user/repo.git"
  },
  "version": "0.0.1",
  "author": {
    "name": "Your Name",
    "email": "user@server.com"
  },
  "license": "MIT",
  "scripts": {
    "build": "webpack --watch --progress"
  },
  "dependencies": {
    "axios": "^0.16.2",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-polyfill": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "css-loader": "^0.28.4",
    "file-loader": "^0.11.2",
    "react": "^15.4.1",
    "react-chrome-redux": "^1.3.3",
    "react-dom": "^15.4.1",
    "react-redux": "^4.4.6",
    "redux": "^3.6.0",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.2.0",
    "url-loader": "^0.5.9"
  },
  "devDependencies": {
    "babel-core": "^6.25.0",
    "babel-loader": "^7.1.1",
    "babel-plugin-transform-async-to-generator": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "copy-webpack-plugin": "^4.0.1",
    "extract-text-webpack-plugin": "^3.0.0",
    "html-webpack-plugin": "^2.30.1",
    "webpack": "^3.5.3",
    "write-file-webpack-plugin": "^3.4.2"
  }
}
```

After installing the packages with either `npm install` or `yarn install`, let's
create the Webpack configuration file.

```js
const path = require('path')
const CopyPlugin = require('copy-webpack-plugin')
const HtmlPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

require("babel-core/register");
require("babel-polyfill");

const PAGES_PATH = './src/pages'

function generateHtmlPlugins(items) {
  return items.map( (name) => new HtmlPlugin(
    {
      filename: `./${name}.html`,
      chunks: [ name ],
    }
  ))
}

module.exports = {
  entry: {
    background: [
      'babel-polyfill',
      `${PAGES_PATH}/background`,
    ],
    popup: [
      'babel-polyfill',
      `${PAGES_PATH}/popup`,
    ]
  },
  output: {
    path: path.resolve('dist/pages'),
    filename: '[name].js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [ 'babel-loader' ]
      },
      {
        test: /\.jpe?g$|\.gif$|\.png$|\.ttf$|\.eot$|\.svg$/,
        use: 'file-loader?name=[name].[ext]?[hash]'
      },
      {
        test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'url-loader?limit=10000&mimetype=application/fontwoff'
      },
    ]
  },
  plugins: [
    new ExtractTextPlugin(
      {
        filename: '[name].[contenthash].css',
      }
    ),
    new CopyPlugin(
      [
        {
          from: 'src',
          to: path.resolve('dist'),
          ignore: [ 'pages/**/*' ]
        }
      ]
    ),
    ...generateHtmlPlugins(
      [
        'background',
        'popup'
      ]
    )
  ]
}
```

Pretty standard setup - output will be stored in the `dist/` folder, that you
should exclude from git, and we'll have two "pages": `background`, which will
hold the persistent Redux store, and `popup` - the browser popup we'll be
interacting with. Accompanying assets (CSS, icons) will be placed on the output
folder as well. To run the build and keep watching for changes, run
`npm run build`, or `yarn build` if you use Yarn.

Finally, before moving onto the actual source, there's one final piece of
plumbing needed, a `.babelrc` file to set up Babel's transpilation behaviour:

```js
{
  "presets": [
    "react",
    "es2015",
    "stage-2"
  ],
  "plugins": [
    "transform-class-properties",
    "transform-async-to-generator",
    "transform-decorators-legacy"
  ]
}
```

With the setup out of the way, let's create our source tree. It should look
something like this:

    ├── src/
    │   ├── assets/
    │   ├── manifest.json
    │   ├── modules/
    │   ├── pages/
    │   │   ├── background/
    │   │   └── popup/
    │   └── shared/
    └── .babelrc
    └── package.json
    └── webpack.config.js

### Background page

Let's start with the background page, since it's the more complex of the two.

This is the content of `src/pages/background/index.js`:

```js
import axios from 'axios'

import store from './store'
```

Pretty simple! [Axios] is a useful promises-based HTTP client that will be really
handy. As for the logic on the `store.js` file:

```js
import { applyMiddleware, createStore } from 'redux'
import { wrapStore, alias } from 'react-chrome-redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk'

import aliases from './aliases'
import reducer from './reducers'

const logger = createLogger({
  collapsed: true,
})

const initialState = {
  lists: {
    activeId: null,
    records: [],
  }
}

const store = createStore(
  reducer,
  initialState,
  applyMiddleware(
    alias(aliases),
    thunk,
    logger,  // NOTE: logger _must_ be last in middleware chain
  ),
)

wrapStore(store, {
  portName: 'BOOKMARKSAVER',
})

export default store
```

Going over each of the parts:

```js
import { applyMiddleware, createStore } from 'redux'
import { wrapStore, alias } from 'react-chrome-redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk'
```

We first import the relevant parts of Redux and React-Chrome-Redux. Loggers are
always handy in development, so we make use of [redux-logger]. Finally, we
import `thunk` to be able to dispatch actions to our store that return other
dispatches. In practice what this means is that we're able to send a whole flow
of actions to the store in the form of promises. This will become clearer ahead.

```js
import aliases from './aliases'
import reducer from './reducers'
```

These imports refer to our reducers, which are typical for a Redux app, and
[aliases] that are a special quirk of React-Chrome-Redux. They are needed since
communication between the parts of our extension via message passing only
supports JSON-serializable objects. Standard FSAs<sup>[1](#fsa)</sup> are
easily serialized onto objects, but thunks (our promise-based flows) are not, so
we create a specific action type to be issued on the popup page that maps onto a
thunk on the background page. More on aliases ahead.

```js
const logger = createLogger({
  collapsed: true,
})

const initialState = {
  lists: {
    activeId: null,
    records: [],
  }
}
```

Here we set options for the logger, and define our initial state for the app.

```js
const store = createStore(
  reducer,
  initialState,
  applyMiddleware(
    alias(aliases),
    thunk,
    logger,  // NOTE: logger _must_ be last in middleware chain
  ),
)

wrapStore(store, {
  portName: 'BOOKMARKSAVER',
})
```

Finally, we create the Redux store passing our reducers, initial state and
middleware we're using. This is just like any other Redux store - on the popup
page we'll need to create a React-Chrome-Redux specific proxy to this store.
Notice also that the message passing port name has to match the one on the popup
page.

Now for the `aliases.js` content:

```js
import {
  LISTS_REFRESH_REQUESTED,
  URL_SUBMIT_REQUESTED,
} from '../../shared/constants'

import { fetchLists, submitURL } from '../../modules/ajax'

const listsRefreshRequestedAlias = (originalAction) => {
  return (dispatch, getState) => {
    fetchLists(dispatch)
  }
}

const urlSubmitRequestedAlias = (originalAction) => {
  const { listId, url, title } = originalAction.payload

  return (dispatch, getState) => {
    submitURL(dispatch, listId, url, title)
  }
}

export default {
  LISTS_REFRESH_REQUESTED: listsRefreshRequestedAlias,
  URL_SUBMIT_REQUESTED: urlSubmitRequestedAlias,
}

```

We start by including some constants. It's a good practice to use constants
instead of strings to refer to actions, since it's less error prone. The
`constants.js` file is straightforward:

```js
export const LISTS_REFRESH_START = 'LISTS_REFRESH_START'
export const LISTS_REFRESH_FULFILLED = 'LISTS_REFRESH_FULFILLED'
export const LISTS_REFRESH_ERRORED = 'LISTS_REFRESH_ERRORED'
export const LISTS_REFRESH_REQUESTED = 'LISTS_REFRESH_REQUESTED'

export const LISTS_SET_ACTIVE = 'LISTS_SET_ACTIVE'

export const URL_SUBMIT_START = 'URL_SUBMIT_START'
export const URL_SUBMIT_FULFILLED = 'URL_SUBMIT_FULFILLED'
export const URL_SUBMIT_ERRORED = 'URL_SUBMIT_ERRORED'
export const URL_SUBMIT_REQUESTED = 'URL_SUBMIT_REQUESTED'
```

Back to our aliases, the important bit is at the end:

```js
export default {
  LISTS_REFRESH_REQUESTED: listsRefreshRequestedAlias,
  URL_SUBMIT_REQUESTED: urlSubmitRequestedAlias,
}
```

This will be used by React-Chrome-Redux to map incoming actions from the popup
to the functions mentioned. Let's see what happens when, for example, our
background store receives a `LISTS_REFRESH_REQUESTED` action.

React-Chrome-Redux will forward that action from the popup to the background
store, but will check defined aliases first. Since we have a function set here,
this code will run:

```js
const listsRefreshRequestedAlias = (originalAction) => {
  return (dispatch, getState) => {
    fetchLists(dispatch)
  }
}
```

`fetchLists` is defined in the `src/modules/ajax.js` file:

```js
export function fetchLists(dispatch) {
  dispatch( (dispatch) => {
    dispatch(listActions.refreshStart())
    axios.get(`${apiHome}/lists`)
      .then( (data) => {
        dispatch(listActions.refreshFulfilled(data))
      })
      .catch( (err) => {
        dispatch(listActions.refreshErrored(err))
      })
  })
}
```

As we can see here, we want to be able to work with promises and execute
follow-up data processing to the fetch result by Axios, or catch any errors.
This flow would not be able to reach our background store if called from the
popup due to the message passing serialization restrictions, hence the need to
"alias" it.

Moving on to the `src/pages/background/reducers.js` file:

```js
import {combineReducers} from 'redux'

import listsReducer from './reducers/listsReducer'

export default combineReducers({
  lists: listsReducer,
})
```

This is the standard Redux way of combining multiple reducers. We only have one
for this example, but adding a new one would be easy - just a new key matching
a new import. To note that as a side-effect of this setup, `listsReducer` will
not receive the whole state whenever processing new actions, only the subset
under the `lists` key in the overall state.

The actual reducer:

```js
import {
  LISTS_REFRESH_FULFILLED,
  LISTS_SET_ACTIVE,
} from '../../../shared/constants'

export default function listsReducer(state={}, action) {
  switch(action.type) {
    case LISTS_SET_ACTIVE:
      state =  {...state, activeId: action.payload}
      break

    case LISTS_REFRESH_FULFILLED:
      const records = action.payload.data.map( (item) => {
        return {
          icon: item.icon,
          name: item.name,
          id: item.id,
        }
      })
      state =  {...state, records: records, activeId: state.activeId || records[0].id}
      break
  }

  return state
}
```

We're handling two cases here: when a list is set to active by being selected on
the popup's UI, and when Axios successfully gets the lists from the remote API.

### Popup page

The popup page is quite simple. Bear in mind that the background page runs as
long as the browser is running, but the popup page is instatiated anew every
time it is interacted with.

Here's `src/pages/popup/index.js`:

```js
import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'react-redux'
import {Store} from 'react-chrome-redux'

import App from './app'

const store = new Store({
  portName: 'BOOKMARKSAVER',
})

store.ready().then(() => {
  const mountNode = document.createElement('div')
  document.body.appendChild(mountNode)

  ReactDOM.render(
    <Provider store={store}>
      <App />
    </Provider>,
    mountNode
  )
})
```

Straightforward setup! Notice that the store we create is not from Redux, but
from React-Chrome-Redux - this is the "proxy store" mentioned before. Since it
is a proxy, the only setting it needs is the port name to reach the background
store through message passing.

After making sure the store is ready after doing a behind-the-scenes initial
sync, we create a `div` element to mount the app on. Let's move on to the meat
of the popup, `src/pages/popup/app.js`.

```js
import React from 'react'
import {connect} from 'react-redux'
import { Dropdown, Button } from 'semantic-ui-react'
import 'semantic-ui-css/semantic.min.css'
```

The basic React imports, plus a little help from [SemanticUI] for element
styling.

```js
import * as listActions from '../../shared/actions/listActions'

import { submitURL } from '../../modules/ajax'

@connect((store) => {
  return {
    lists: store.lists,
  }
})
```

Next up, we import our list action creators and the `submitURL` function from
our shared AJAX module. We also `connect` the component to the Redux store using
decorators - one of the plugins we've added to our `.babelrc` (
"transform-decorators-legacy"). The connection to the store makes the `lists`
branch of our state tree available to the class as a prop.

```js
export default class App extends React.Component {
  componentWillMount() {
    const { lists, dispatch } = this.props

    if (lists.records.size === undefined) {
      dispatch(listActions.requestRefresh())
    }
  }
}
```

Using one of React's [lifecycle methods] we make sure that we dispatch an action
to refresh our lists from the server. This will be done on the first instance we
interact with the popup, as that first time will populate the state for our
background store. So subsequent instances of the popup page will just sync up
with that state in the store.

Let's go over the main render method.

```js
render() {
  const { lists } = this.props

  const options = lists.records.map( (record) => {
    return {
      key: record.id,
      text: record.name,
      value: record.id,
      icon: record.icon,
    }
  })

  return (
    <div className="ui grid divided container">
      <div className="ui one wide column">
        <br/>
        <Dropdown
          onChange={(_evt, data) => this.listChanged(data)}
          selection
          placeholder='Select List'
          options={options}
          defaultValue={this.optionWithDefaultValue(options)}
        />
        <div className="ui divider"/>
        <Button primary onClick={
          () => this.submitCurrentURL()
        }>Save</Button>
      </div>
    </div>
  )
}
```

We iterate over our lists and create an `options` array, that is then fed into a
`Dropdown` SemanticUI component. The two handlers we setup cover changing the
selected list (`onChange` event for the dropdown) and submitting the current
URL (`onClick` for the submit button).

When the list is changed, we dispatch an action to change the default one. We've
covered the reducer above:

```js
listChanged(data) {
  const { dispatch } = this.props

  dispatch(
    listActions.setActive(data.value)
  )
}
```

And when we submit the current URL, we just use Chrome's [tabs API] to get the
current one's properties, and use that as a payload to the URL submit action:

```js
async submitCurrentURL() {
  const { dispatch, lists } = this.props
  const activeListId = lists.activeId

  chrome.tabs.query({
    active: true,
    currentWindow: true
  }, (arrayOfTabs) => {
    const activeTab = arrayOfTabs[0]
    const url = activeTab.url
    const title = activeTab.title

    dispatch(
      listActions.requestURLSubmit(activeListId, url, title)
    )
    window.close()
  })
}
```

To see the extension in action, enable "developer mode" on Chrome's extensions
page, and then "Load unpacked extension" and point it to the `dist` folder that
webpack's build creates. The extension should be now added to your list.

![](https://images.thoughtbot.com/blog-vellum-image-uploads/WvamalvUR3qgI5dIwr7p_screenshot.png)

To use the extension, you need a server providing the simple API it consumes.
Let's mock it using [json-server]. Install it with `npm`, then create a JSON
file with this content:

```js
{
  "lists": [
    {
      "id": 1,
      "icon": "inbox",
      "name": "Inbox"
    },
    {
      "id": 2,
      "icon": "rss",
      "name": "RSS"
    }
  ]
}
```

Save it as `db.json` and run it with `json-server --port 5000 --watch db.json`.

We can now use the extension. Whenever you save a link with it, you should see a
`POST` on the terminal window you're running `json-server` on:

<pre>
<samp>OPTIONS /lists/1/list_items 204 0.462 ms - 0
POST /lists/1/list_items 404 13.328 ms - 2</samp>
</pre>

Once added to Chrome, you can navigate to any URL and click the extension's icon
to bring up the popup.

![](https://images.thoughtbot.com/blog-vellum-image-uploads/Fgfd2n8UQ2CxtZRr0Lie_ezgif-4-0a5eb9dda8.gif)

And that's it! This is a bit of a contrived example, but serves as a good intro
to how Redux can help manage state in your Chrome extensions, and also on how
to separate interactions within your extension into clearly delineated actions.
Full code for this article is available [here].

To note, background pages have the potential to negatively impact your browser's
performance and your device's battery life. [Event pages] are preferable, and I
might revisit that topic in a follow-up post. For now, have a look [at this] to
have a feel for a potential solution using events.

A helpful tool as an extension developer is [Quick Extension Reload] that adds a
very helpful button to the browser's right-click menu to reload extensions,
which is much handier than going to the extension settings page.

&nbsp;

<a name="fsa">1</a>: Have a look at [Flux Standard Action] for more on how to
structure your action objects.

[at this]: https://github.com/tshaddix/next-read/blob/master/event/src/index.js#L11-L20
[Event pages]: https://developer.chrome.com/extensions/event_pages
[React]: https://facebook.github.io/react/
[here]: https://github.com/thoughtbot/bookmark_saver
[Quick Extension Reload]: https://chrome.google.com/webstore/detail/quick-extension-reload/goeiakeofnlpkioeadcbocfifmgkidpb
[json-server]: https://www.npmjs.com/package/json-server
[tabs API]: https://developer.chrome.com/extensions/tabs
[redux-logger]: https://www.npmjs.com/package/redux-logger
[Axios]: https://www.npmjs.com/package/axios
[aliases]: https://github.com/tshaddix/react-chrome-redux#3-optional-implement-actions-whose-logic-only-happens-in-the-background-script-we-call-them-aliases
[overview]: https://github.com/tshaddix/react-chrome-redux#overview
[message passing]: https://developer.chrome.com/extensions/messaging
[background script]: https://developer.chrome.com/extensions/background_pages
[popup]: https://developer.chrome.com/extensions/browserAction
[four functions]: http://redux.js.org/docs/api/Store.html
[Flux Standard Action]: https://github.com/acdlite/flux-standard-action
[how to write a Chrome extension]: https://thoughtbot.com/blog/how-to-make-a-chrome-extension
[Flux]: https://facebook.github.io/react/blog/2014/05/06/flux.html
[official docs]: http://redux.js.org/docs/introduction/
[playlist on Redux]: https://www.youtube.com/playlist?list=PLoYCgNOIyGADILc3iUJzygCqC8Tt3bRXt
[Redux]: http://redux.js.org/
[theme extension]: https://developer.chrome.com/extensions/themes
[SemanticUI]: https://react.semantic-ui.com/introduction
[lifecycle methods]: https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle
