reduxed-chrome-storage
TypeScript icon, indicating that this package has built-in type declarations

3.0.10 • Public • Published

Reduxed Chrome Storage

Redux interface to chrome.storage (browser.storage). A unified way to use Redux in all modern browser extensions. The only way to get Redux working in Manifest V3 Chrome extensions.

Related article

Table of contents

Installation

npm install reduxed-chrome-storage

How To Use

Common use case

import {
  setupReduxed, ReduxedSetupOptions
} from 'reduxed-chrome-storage';
import { configureStore } from '@reduxjs/toolkit';
import reducer from './reducer';

const storeCreatorContainer = (preloadedState?: any) =>
  configureStore({reducer, preloadedState});
const options: ReduxedSetupOptions = { ... };
const instantiate = setupReduxed(storeCreatorContainer, options);
// Obtain a replacement store instance
//   Option #1: Promise style
instantiate().then(store => {
  const state = store.getState();
  ...
});
//   Option #2: async/await style
async () => {
  const store = await instantiate();
  const state = store.getState();
  ...
}

Special use case: Manifest V3 service worker

import {
  setupReduxed, diffDeep, isEqual,
  ReduxedSetupOptions, ReduxedSetupListeners, ChangeListener
} from 'reduxed-chrome-storage';
...

// In order to track state changes set onGlobalChange and(or) onLocalChange
// properties within 3rd argument of setupReduxed like below
// instead of calling store.subscribe() in each API event listener

const globalChangeListener: ChangeListener = (store, previousState) => {
  const state = store.getState();
  // Below is an example how to filter down state changes
  // by comparing current state with previous one
  const diff = diffDeep(state, previousState);
  if (diff && ['someKey', 'anotherKey'].some(key => key in diff)) {
    ...
  }
};

const localChangeListener: ChangeListener = (store, previousState) => {
  const state = store.getState();
  // Another (a simpler) example how to filter down state changes
  if (isEqual(state.someKey, previousState.someKey)) {
    ...
  }
};

const storeCreatorContainer = ...;
const options: ReduxedSetupOptions = { ... };
const listeners: ReduxedSetupListeners = {
  onGlobalChange: globalChangeListener,
  onLocalChange: localChangeListener
};
const instantiate = setupReduxed(storeCreatorContainer, options, listeners);

// General pattern
chrome.{API}.on{Event}.addListener(async () => {
  // Obtain a store instance
  const store = await instantiate();
  const state = store.getState();
  ...
});

// Specific example
chrome.runtime.onStartup.addListener(async () => {
  // Obtain a store instance
  const store = await instantiate();
  const state = store.getState();
  ...
});
...

Tracking errors

import {
  setupReduxed, ReduxedSetupListeners, ErrorListener
} from 'reduxed-chrome-storage';
...

// errorListener will be called whenever an error occurs
// during chrome.storage update
const errorListener: ErrorListener = (message, exceeded) => {
  ...
};
const storeCreatorContainer = ...;
const listeners: ReduxedSetupListeners = {
  onError: errorListener
};
const instantiate = setupReduxed(storeCreatorContainer, { ... }, listeners);
...

setupReduxed() function

This library exports four named functions: setupReduxed() as well as three utility functions to be described later. As its name suggests, setupReduxed() function sets up Reduxed Chrome Storage allowing to get a Redux store replacement connected to the state in chrome.storage (here and below chrome.storage means both chrome.storage itself and Webextensions' browser.storage). setupReduxed() receives three arguments (to be described below) and returns an async function (TBD below too).

Note: setupReduxed() must only be called once per extension component (popup, content script etc.).

Store creator container

setupReduxed() expects its first argument (the only mandatory one) to be a store creator container. Store creator container is a function that calls a store creator and returns the result that is a Redux store. It receives one argument to be passed as the preloadedState argument into the store creator. Store creator is either the Redux's createStore() or any function that wraps the createStore(), e.g. configureStore() of Redux Toolkit.

Options

setupReduxed() allows to customize the setup via options. Options are specified as named properties within optional second argument. Below is the list of available options.

namespace

Type: string
Default: 'chrome'

A string to identify the APIs namespace to be used, either 'chrome' or 'browser'. If this and the next two options are missing, the chrome namespace is used by default.

chromeNs

Type: host object (ChromeNamespace in Typescript definition)

The chrome namespace within Manifest V2 extension. If this option is supplied, the previous one is ignored.

browserNs

Type: host object (BrowserNamespace in Typescript definition)

The browser namespace within Firefox extension, or the chrome namespace within Manifest V3 chrome extension. You may pass the chrome namespace within Manifest V3 to make this library use Promise-based APIs under the hood. If this option is supplied, the previous two are ignored.

storageArea

Type: string
Default: 'local'

The name of chrome.storage area to be used, either 'sync' or 'local'. Note: it is not recommended to use sync area for immediately storing the state of extension. Use local area instead - it has less strict limits than sync. If you need to sync the state (entirely or partially) to a user's account, create a temporary store of sync area, then copy the needed data to (or from) the main store (of local area).

storageKey

Type: string
Default: 'reduxed'

Key under which the state will be stored/tracked in chrome.storage.

isolated

Type: boolean
Default: false

Check this option if your store in this specific extension component isn't supposed to receive state changes from other extension components. It is recommended to always check this option in Manifest V3 service worker and all extension-related pages (e.g. options page etc.) except popup page.

plainActions

Type: boolean
Default: false

Check this option if your store is only supposed to dispatch plain object actions.

outdatedTimeout

Type: number
Default: 1000

Max. time (in ms) to wait for outdated (async) actions to be completed (see How It Works section for details). This option is ignored if at least one of the previous two (isolated/plainActions) options is checked.

Listeners

setupReduxed() also allows to specify an error listener as well as two kinds of state change listeners. These listeners are specified as named properties within optional third argument. Below are their descriptions.

onError

Type: function (ErrorListener in Typescript definition)

A function to be called whenever an error occurs during chrome.storage update. Receives two arguments:

  1. an error message defined by storage API;
  2. a boolean indicating if the limit for the used storage area is exceeded.

onGlobalChange

Type: function (ChangeListener in Typescript definition)

A function to be called whenever the state changes that may be caused by any extension component (popup etc.). Receives two arguments:

  1. a temporary store representing the current state;
  2. the previous state.

onLocalChange

Type: function (ChangeListener in Typescript definition)

A function to be called whenever a store in this specific extension component (obviously a service worker) causes a change in the state. Receives two arguments:

  1. reference to the store that caused this change in the state;
  2. the previous state.

onGlobalChange (that gets state updates from all extension components) has a larger coverage than onLocalChange (that only gets state updates from specific extension component). However onLocalChange has the advantage that it gets state updates immediately once they're done, unlike onGlobalChange that only gets state updates after they're passed through chrome.storage update cycle (storage.set call, then storage.onChanged event firing) which takes some time.

onGlobalChange/onLocalChange listeners only make sense in Manifest V3 service workers where they're to be used as a replacement of store.subscribe. In all other extension components (MV2 background scripts, popups etc.) the standard store.subscribe is supposed to be used (mostly indirectly) for tracking state changes.

The returning function

setupReduxed() returns a function that creates asynchronously a Redux store replacement connected to the state stored in chrome.storage.

Receives one optional argument of any type: some value to which the state will be reset entirely or partially upon the store replacement creation.

Returns a Promise to be resolved when the created replacement store is ready.

Note: Like setupReduxed(), the returning function must only be called once per extension component. The only exception is a Manifest V3 service worker, where the returning function is to be called in each API event listener (see How To Use section).

Utility functions

Aside from setupReduxed(), this library also exports three utility functions optimised to work with JSON data: isEqual, diffDeep and cloneDeep. One may use these functions to compare/copy state-related data (see How To Use section).

isEqual

Checks deeply if two supplied values are equal.

Receives: two arguments - values to be compared.

Returns: a boolean.

diffDeep

Finds the deep difference between two supplied values.

Receives: two arguments - values to be compared.

Returns: the found deep difference.

cloneDeep

Creates a deep copy of supplied value using JSON stringification.

Receives: one argument - a value to copy.

Returns: the created deep copy.

How It Works / Caveats

JSON stringification of the state

With this library all the state is stored in chrome.storage. As known, all data stored in chrome.storage are JSON-stringified. This means that the state should not contain non-JSON values as they are to be lost anyway (to be removed or replaced with JSON primitive values). Using non-JSON values in the state may cause unwanted side effects. This is especially true for object properties explicitly set to undefined ( {key: undefined, ...} ).

Internal store, external state updates and outdated actions

In order to ensure full compatibility with Redux API this library uses an internal Redux store - true Redux store created by the supplied store creator container upon initialisation of the replacement Redux store. Every Redux action dispatched on a replacement Redux store (returned by the setupReduxed() returning function) is forwarded to its internal Redux store that dispatches this action. As a result the current state in the replacement store is updated (synced) with the result state in the internal store. If a dispatching action is a plain object, the respective state update/change comes to the replacement store immediately. Otherwise, i.e. if this is an async action, it may take some time for the replacement store to receive the respective state update.

Such state updates as above (caused and received within the same store / extension component) are called local. But there can also be external state updates - caused by stores in other extension components. Whenever a replacement store receives an external state update its internal Redux store is re-created (renewed) with a new state that is the result of merging the current state with the state resulted from the external state update (note that only object literals are actually merged, all other values incl. arrays are replaced). Such re-creation is the only way to address external state updates as they come via chrome.storage API that doesn't provide info (action or series of actions) how to reproduce this state update/change, only the result state. Once the re-creation is done, the former current state is lost (replaced with a new one). But the former internal Redux store isn't necessarily lost. If there are uncompleted (async) actions belonging to (initiated by) this store, they all (incl. the store) are stored in a sort of temporary memory. Such uncompleted actions, belonging to internal Redux store that is no longer actual, are called outdated. Outdated actions belonging to the same store are stored in the temporary memory until the timeout specified by outdatedTimeout option is exceeded (the time is counted from the moment the store was re-created i.e. became outdated). If an outdated action is completed before outdatedTimeout period has run out, the respective result state is to be merged with the current state of the replacement store. Otherwise this (still uncompleted) action is to be lost, along with the store it belongs to (and all other still uncompleted actions within this store).

License

Licensed under the MIT license.

Package Sidebar

Install

npm i reduxed-chrome-storage

Weekly Downloads

247

Version

3.0.10

License

MIT

Unpacked Size

81.2 kB

Total Files

11

Last publish

Collaborators

  • hindmost