Using Redux in Event-Driven Chrome Extensions: Problem/Solution

Savr Goryaev
Level Up Coding
Published in
9 min readJul 5, 2020

--

This post is targeted at experienced web developers and addresses the problem of using Redux in so called event-driven extensions - Chrome extensions implementing the event-based model.

Specifics of event-driven extensions

The event-based model of an extension was first introduced in Chrome 22 released in 2012. It assumes that a background script (if any) is only loaded/running when it is needed (mainly in response to events) and unloaded when it goes idle.

The Chrome Developer Docs strongly advise to migrate existing persistent extensions to event-based model and use it for new extensions always with the only exception. But it seems that many extensions keep being persistent nowadays even if they could be event-driven. Of course, many of them were first released before the event-based model became known to developers. And now their authors just have no incentive to migrate to new model. It’s even worse as it implies a lot of changes to be made almost everywhere, not only in a background script.

Furthermore, it’s pretty common for extension developers to use cross-browser approach by building extensions for different browsers from the same source code. Event-based model is a Chrome-specific feature which has major differences from the persistent model supported by most browsers. It makes cross-browser development a pretty hard problem.

However some of relatively new extensions also keep using persistent model, though they could be potential event-driven ones. Eventually the main reason is the same as for the migration case: major differences between event-based and persistent model, which reflected primarily in how extension’s state can be handled/managed.

Problem with Redux

State management is one of those things that have special value for modern web development. Every web application (as well as browser extension), once it becomes complex enough, needs a unified and preferably simple way to manage its state. And this is where Redux shines. Redux is a popular library that helps manage the state of an application in a consistent and quite simple way. The trouble is that Redux is not supposed to work with Chrome extensions.

First of all, there is a problem inherent to all extensions (not only Chrome ones), that is related to one of the fundamental Redux rules called “single source of truth”, which requires all the state to be stored in the same place. Extensions usually consist of multiple separate components (such as background or content scripts) that makes it hard to comply with this rule. Fortunately, this problem is solvable and there are such libraries as Webext Redux that solve it using message passing. Unfortunately, this solution can’t be applied to event-driven extensions due to already mentioned differences between event-based and persistent model. Now it’s time to take a close look at these differences and effects caused by them.

In a persistent model, it’s common to hold the state of extension (usually in some local variable) inside persistent background script that lives/runs till browser’s closed, so the state is always available to other extension components (e.g. popups or content scripts) via background script, which thereby plays a role of server. This is a standard persistent approach used by libraries like Webext Redux.

Persistent approach to state management

Regarding event-based model, there cannot be a server like above, because all components including background script are equal from the viewpoint of lifetime. To be more specific, one cannot store the state in a background script as the latter may be unloaded from memory at any moment (that can’t be known beforehand). Luckily this problem also has solution.

Solution

The solution is to use chrome.storage as an immediate place/way of storing/manipulating the state of extension. This approach, by the way explicitly suggested by official migration guide, assumes that the state is stored immediately in chrome.storage, which API is called whenever there is a need to change the state or track such changes.

chrome.storage-based approach to state management

As an integrated part of Extensions API, chrome.storage has a number of advantages, and the most important of them is that any extension component may have direct access to it without any intermediation. Another advantage (and at the same time a part of its specification) is state persistence through browser sessions, that is its built-in feature working out-the-box.

It’s also worth noting that this approach works (in the same way) in both event-based and persistent model. So in the end one has a unified solution supported by most browsers, which makes cross-browser development (a bit) more reasonable.

The only problem left is that chrome.storage has differences from Redux, which makes it impossible to use it in the Redux way. Sure, one can use chrome.storage as is, or write some custom wrapper for it. However, Redux became some sort of standard in state management nowadays. So it would be better to somehow adapt chrome.storage to Redux rules, or in other words, get Redux from chrome.storage).

Our objective in this post is to make Redux-compatible interface for chrome.storage, which will translate its behaviour into terms of Redux. In terms of API we need to implement interface of Redux functionality that immediately deals with Redux store. It includes createStore function, as well as Store object (representing Redux store) returned by it. Below are their interfaces expressed in JSDoc tags:

Note: There is also Store.replaceReducer method which is unneeded in our case because of built-in state persistence feature. Supplementary functions like combineReducers or applyMiddleware are also unneeded, as they don’t immediately deal with Redux store.

Implementation

So we have to write a class implementing Store interface. Let’s call it ReduxedStorage.

Implementing getState and subscribe methods within our class is quite simple as they have close counterparts in chrome.storage: get method and onChanged event. Sure, they won’t be used as direct replacement for respective Store methods, but they will help to keep up-to-date a local copy of the state in our class. We can initialize the state in our class by calling chrome.storage’s get method at ReduxedStorage creation/instantiation and then, whenever onChanged event fires, accordingly update the state. Thereby we ensure that the state is always up to date. Then getState will be a trivial getter within our class. Implementation of subscribe method is a bit harder: it will add a listener function to some array of listeners to be called whenever onChanged event fires.

Unlike getState and subscribe, chrome.storage has nothing like dispatch method. Direct usage of such chrome.storage method as set is incompatible with Redux principles: in Redux the state is only set once, at store creation, and then it can only be changed via dispatch calls. So we have to somehow reproduce the behaviour of Store.dispatch in our ReduxedStorage class. There are two ways to do it. Radical one would imply actual reproduction of relevant Redux functionality laid over chrome.storage API. But there is also a compromise option that will be used in this post.

The key idea is to instantiate a new Redux store internally whenever some action is dispatched in our class. Sure it looks a bit weird, but this is the only alternative to actual reproduction. To be more specific, whenever our dispatch method is called, we have to instantiate a new Redux store by calling actual Redux’s createStore function, initialize its state with the current state of our class and call Store.dispatch passing the action argument our dispatch method called with. Also, on the same Redux store, we have to add a one-time change listener to update chrome.storage with a new state resulted from the dispatched action (once it’s ready). Each such update should be tracked and handled by our chrome.storage.onChanged listener described above.

Just a few notes about the state initialization: Since chrome.storage’s get method runs asynchronously, we cannot call it inside constructor. So we have to place the relevant code in a separate method to be called immediately after constructor. This method, let’s call it init, will return a promise to be resolved upon get completion. Within init method we also need another actual Redux store to be instantiated in order to get default state value to be used as a fallback if chrome.storage is currently empty.

So this is how ReduxedStorage class might look in first approximation:

Note: we have to use a part of chrome.storage data under specific key (specified by this.key constant) in order to have an option to get a new state immediately in chrome.storage.onChanged listener, without extra chrome.storage’s get call. Furthermore, it also useful if the state is supposed to be represented as an array, since chrome.storage only allows a plain object to be stored at the root level.

Unfortunately the above implementation has an unobvious flaw which is caused by that we update this.state property indirectly, via chrome.storage’s set method in conjunction with chrome.storage.onChanged listener. This in itself is not a problem. However, creating a Redux store inside dispatch method relies on this.state, which may be a problem because this.state may not always reflect the actual state. This may be so if one dispatches multiple actions synchronously in a row. In this case the 2nd and all subsequent dispatch calls deal with outdated data in this.state, which is not updated yet at the moment of call because of asynchronous nature of chrome.storage’s set method. Thus, dispatching multiple synchronous actions may lead to unexpected/unwanted results.

In order to fix the above problem one can modify dispatch method to reuse the same Redux store (as well as the related state) for such multiple actions. Such buffered store has to be reset/recreated after some small time-out period, let it be 100 msec by default. It means that we have to allocate additional class properties for the buffered Redux store and the related state. Below is how such buffered dispatch version might look:

As often happens, fixing one problem may cause another one. In our case using buffered reusable store instead of local one-time one may break async logic within Redux. Async logic that isn’t featured in Redux by default, may be introduced using middleware such as Redux Thunk. With Redux Thunk one may delay the dispatch of an action by writing an action creator that returns a function instead of an action. Below is an example of such action creator:

delayAddTodo delays the dispatch of ADD_TODO action for 1 sec.

If we try to use this action creator with the above buffered dispatch method, we get an error at the time of calling this.buffStore.getState inside this.buffStore.subscribe callback. This is because this.buffStore.subscribe callback in this specific case is to be called at least 1 sec after dispatch call, when this.buffStore is already reset to null (100 msec since dispatch call). In contrast, the previous `dispatch` version works fine with such async action creators (as well as single sync ones) cause it uses a local store that is always available to the related subscribe callback.

So we have to combine two approaches, i.e. use both buffered and local versions of Redux store. The former will be used for sync actions and the latter — for async ones that take some time like the above delayAddTodo. It doesn’t mean however that we need two separate Redux store instances in one dispatch call. We can instantiate a Redux store once in this.buffStore class property (corresponding to the buffered version), and then copy its reference to a local variable, let’s call it lastStore. So when this.buffStore is reset to null, lastStore should still refer to the same Redux store and be available to the related subscribe callback. Therefore we can use lastStore inside inner subscribe listener as a fallback if this.buffStore is unavailable which mean an async action. When a state change is processed in the inner subscribe callback, it’d be useful to unsubscribe given callback/listener and reset lastStore variable in order to release the related resources.

Furthermore, it’d be nice to make some improvements/refactoring in the overall code such as:

  • make this.areaName and this.key properties variable/customizable.
  • move the code immediately calling chrome.storage API to a separate class, let’s call it WrappedStorage.

So below is the result implementation:

Its usage is similar to original Redux except that our store creator is wrapped in a customizer function and works in async way returning a promise instead of a new store, which is due to asynchronous nature of chrome.storage API.

Standard usage looks like this:

Also, with async/await feature available (since ES 2017), our interface can be used in an advanced way like this:

Here is the source code of the library we just made. You can use it for your needs.

It is also available as NPM package:

npm install reduxed-chrome-storage

--

--