{"id":13681117,"url":"https://github.com/tshaddix/webext-redux","last_synced_at":"2025-05-13T22:04:31.929Z","repository":{"id":3805937,"uuid":"50215418","full_name":"tshaddix/webext-redux","owner":"tshaddix","description":"A set of utilities for building Redux applications in Web Extensions.","archived":false,"fork":false,"pushed_at":"2025-02-18T21:38:09.000Z","size":664,"stargazers_count":1234,"open_issues_count":47,"forks_count":183,"subscribers_count":37,"default_branch":"master","last_synced_at":"2025-04-26T00:33:07.364Z","etag":null,"topics":["chrome-extension","react-chrome-extension","react-chrome-redux","redux","redux-application","redux-store","web-extension"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tshaddix.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-01-23T00:09:09.000Z","updated_at":"2025-04-25T20:51:54.000Z","dependencies_parsed_at":"2024-06-15T16:45:01.150Z","dependency_job_id":"4a4d86f2-0609-4aeb-bf6e-77551ffde948","html_url":"https://github.com/tshaddix/webext-redux","commit_stats":{"total_commits":243,"total_committers":43,"mean_commits":5.651162790697675,"dds":0.6666666666666667,"last_synced_commit":"dbfb07fb4a562a693dbce82af82d433efa4d0101"},"previous_names":["tshaddix/react-chrome-redux","tshaddix/react-chromex-redux"],"tags_count":34,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tshaddix%2Fwebext-redux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tshaddix%2Fwebext-redux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tshaddix%2Fwebext-redux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tshaddix%2Fwebext-redux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tshaddix","download_url":"https://codeload.github.com/tshaddix/webext-redux/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251315847,"owners_count":21569870,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chrome-extension","react-chrome-extension","react-chrome-redux","redux","redux-application","redux-store","web-extension"],"created_at":"2024-08-02T13:01:26.595Z","updated_at":"2025-04-28T15:17:22.848Z","avatar_url":"https://github.com/tshaddix.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# WebExt Redux\nA set of utilities for building Redux applications in web extensions. This package was originally named `react-chrome-redux`.\n\n[![NPM Version][npm-image]][npm-url]\n[![NPM Downloads][downloads-image]][downloads-url]\n\n## Installation\n\nThis package is available on [npm](https://www.npmjs.com/package/webext-redux):\n\n```\nnpm install webext-redux\n```\n\n## Overview\n\n`webext-redux` allows you to build your Web Extension like a Redux-powered webapp. The background page holds the Redux store, while Popovers and Content-Scripts act as UI Components, passing actions and state updates between themselves and the background store. At the end of the day, you have a single source of truth (your Redux store) that describes the entire state of your extension.\n\nAll UI Components follow the same basic flow:\n\n1. UI Component dispatches action to a Proxy Store.\n2. Proxy Store passes action to background script.\n3. Redux Store on the background script updates its state and sends it back to UI Component.\n4. UI Component is updated with updated state.\n\n![Architecture](https://cloud.githubusercontent.com/assets/603426/18599404/329ca9ca-7c0d-11e6-9a02-5718a0fba8db.png)\n\n## Basic Usage ([full docs here](https://github.com/tshaddix/webext-redux/wiki))\n\nAs described in the [introduction](https://github.com/tshaddix/webext-redux/wiki/Introduction#webext-redux), there are two pieces to a basic implementation of this package.\n\n### 1. Add the *Proxy Store* to a UI Component, such as a popup\n\n```js\n// popover.js\n\nimport React from 'react';\nimport {render} from 'react-dom';\nimport {Provider} from 'react-redux';\nimport {Store} from 'webext-redux';\n\nimport App from './components/app/App';\n\nconst store = new Store();\n\n// wait for the store to connect to the background page\nstore.ready().then(() =\u003e {\n  // The store implements the same interface as Redux's store\n  // so you can use tools like `react-redux` no problem!\n  render(\n    \u003cProvider store={store}\u003e\n      \u003cApp/\u003e\n    \u003c/Provider\u003e\n    , document.getElementById('app'));\n});\n```\n\n### 2. Wrap your Redux store in the background page with `wrapStore()`\n\n```js\n// background.js\n\nimport {createWrapStore} from 'webext-redux';\n\nconst store; // a normal Redux store\n\nconst wrapStore = createWrapStore()\nwrapStore(store);\n```\n\nThat's it! The dispatches called from UI component will find their way to the background page no problem. The new state from your background page will make sure to find its way back to the UI components.\n\n\u003e [!NOTE]\n\u003e `createWrapStore()` ensures webext-redux can handle events when the service worker restarts. It must be called statically in the global scope of the service worker. In other words, it shouldn't be nested in async functions, just like [any other Chrome event listeners](https://developer.chrome.com/docs/extensions/get-started/tutorial/service-worker-events#step-5).\n\n\n### 3. Optional: Apply any redux middleware to your *Proxy Store* with `applyMiddleware()`\n\n\nJust like a regular Redux store, you can apply Redux middlewares to the Proxy store by using the library provided applyMiddleware function.  This can be useful for doing things such as dispatching thunks to handle async control flow.\n\n```js\n// content.js\nimport {Store, applyMiddleware} from 'webext-redux';\nimport thunkMiddleware from 'redux-thunk';\n\n// Proxy store\nconst store = new Store();\n\n// Apply middleware to proxy store\nconst middleware = [thunkMiddleware];\nconst storeWithMiddleware = applyMiddleware(store, ...middleware);\n\n// You can now dispatch a function from the proxy store\nstoreWithMiddleware.dispatch((dispatch, getState) =\u003e {\n  // Regular dispatches will still be routed to the background\n  dispatch({ type: 'start-async-action' });\n  setTimeout(() =\u003e {\n    dispatch({ type: 'complete-async-action' });\n  }, 0);\n});\n```\n\n\n\n### 4. Optional: Implement actions whose logic only happens in the background script (we call them aliases)\n\n\nSometimes you'll want to make sure the logic of your action creators happen in the background script. In this case, you will want to create an alias so that the alias is proxied from the UI component and the action creator logic executes in the background script.\n\n```js\n// background.js\n\nimport { applyMiddleware, createStore } from 'redux';\nimport { alias } from 'webext-redux';\n\nconst aliases = {\n  // this key is the name of the action to proxy, the value is the action\n  // creator that gets executed when the proxied action is received in the\n  // background\n  'user-clicked-alias': () =\u003e {\n    // this call can only be made in the background script\n    browser.notifications.create(...);\n\n  };\n};\n\nconst store = createStore(rootReducer,\n  applyMiddleware(\n    alias(aliases)\n  )\n);\n```\n\n```js\n// content.js\n\nimport { Component } from 'react';\n\nconst store = ...; // a proxy store\n\nclass ContentApp extends Component {\n  render() {\n    return (\n      \u003cinput type=\"button\" onClick={ this.dispatchClickedAlias.bind(this) } /\u003e\n    );\n  }\n\n  dispatchClickedAlias() {\n    store.dispatch({ type: 'user-clicked-alias' });\n  }\n}\n```\n\n### 5. Optional: Retrieve information about the initiator of the action\n\nThere are probably going to be times where you are going to want to know who sent you a message. For example, maybe you have a UI Component that lives in a tab and you want to have it send information to a store that is managed by the background script and you want your background script to know which tab sent the information to it. You can retrieve this information by using the `_sender` property of the action. Let's look at an example of what this would look like.\n\n```js\n// actions.js\n\nexport const MY_ACTION = 'MY_ACTION';\n\nexport function myAction(data) {\n    return {\n        type: MY_ACTION,\n        data: data,\n    };\n}\n```\n\n```js\n// reducer.js\n\nimport {MY_ACTION} from 'actions.js';\n\nexport function rootReducer(state = ..., action) {\n    switch (action.type) {\n    case MY_ACTION:\n        return Object.assign({}, ...state, {\n            lastTabId: action._sender.tab.id\n        });\n    default:\n        return state;\n    }\n}\n```\n\nNo changes are required to your actions, webext-redux automatically adds this information for you when you use a wrapped store.\n\n## Migrating from regular Redux\n\n### 1. dispatch\n\nContrary to regular Redux, **all** dispatches are asynchronous and return a `Promise`.\nIt is inevitable since proxy stores and the main store communicate via browser messaging, which is inherently asynchronous.\n\nIn pure Redux, dispatches are synchronous \n(which may not be true with some middlewares such as `redux-thunk`).\n\nConsider this piece of code:\n```js\nstore.dispatch({ type: MODIFY_FOO_BAR, value: 'new value'});\nconsole.log(store.getState().fooBar);\n```\n\nYou can rely that `console.log` in the code above will display the modified value.\n\nIn `webext-redux` on the Proxy Store side you will need to \nexplicitly wait for the dispatch to complete:\n\n```js\nstore.dispatch({ type: MODIFY_FOO_BAR, value: 'new value'}).then(() =\u003e \n    console.log(store.getState().fooBar)\n);\n```\nor, using async/await syntax:\n\n```js\nawait store.dispatch({ type: MODIFY_FOO_BAR, value: 'new value'});\nconsole.log(store.getState().fooBar);\n```\n\n### 2. dispatch / React component updates\n\nThis case is relatively rare.\n\nOn the Proxy Store side, React component updates with `webext-redux` \nare more likely to take place after a dispatch is started and before it completes.\n\nWhile the code below might work (luckily?) in classical Redux, \nit does not anymore since the component has been updated before the `deletePost` is fully completed\nand `post` object is not accessible anymore in the promise handler:\n```js\nclass PostRemovePanel extends React.Component {\n    (...)\n    \n    handleRemoveButtonClicked() {\n        this.props.deletePost(this.props.post)\n          .then(() =\u003e {\n            this.setState({ message: `Post titled ${this.props.post.title} has just been deleted` });\n          });\n    }\n}\n```\nOn the other hand, this piece of code is safe:\n\n```js\n    handleRemoveButtonClicked() {\n        const post = this.props.post;\n        this.props.deletePost(post);\n          .then(() =\u003e {\n            this.setState({ message: `Post titled ${post.title} has just been deleted` });\n          });\n        }\n    }\n```\n\n### Other\n\nIf you spot any more surprises that are worth watching out for, make sure to let us know!\n\n## Custom Serialization\n\nYou may wish to implement custom serialization and deserialization logic for communication between the background store and your proxy store(s). Web Extension's message passing (which is used to implement this library) automatically serializes messages when they are sent and deserializes them when they are received. In the case that you have non-JSON-ifiable information in your Redux state, like a circular reference or a `Date` object, you will lose information between the background store and the proxy store(s). To manage this, both `wrapStore` and `Store` accept `serializer` and `deserializer` options. These should be functions that take a single parameter, the payload of a message, and return a serialized and deserialized form, respectively. The `serializer` function will be called every time a message is sent, and the `deserializer` function will be called every time a message is received. Note that, in addition to state updates, action creators being passed from your content script(s) to your background page will be serialized and deserialized as well.\n\n### Example\nFor example, consider the following `state` in your background page:\n\n```js\n{todos: [\n    {\n      id: 1,\n      text: 'Write a Web extension',\n      created: new Date(2018, 0, 1)\n    }\n]}\n```\n\nWith no custom serialization, the `state` in your proxy store will look like this:\n\n```js\n{todos: [\n    {\n      id: 1,\n      text: 'Write a Web extension',\n      created: {}\n    }\n]}\n```\n\nAs you can see, Web Extension's message passing has caused your date to disappear. You can pass a custom `serializer` and `deserializer` to both `wrapStore` and `Store` to make sure your dates get preserved:\n\n```js\n// background.js\n\nimport {createWrapStore} from 'webext-redux';\n\nconst wrapStore = createWrapStore();\nconst store; // a normal Redux store\n\nwrapStore(store, {\n  serializer: payload =\u003e JSON.stringify(payload, dateReplacer),\n  deserializer: payload =\u003e JSON.parse(payload, dateReviver)\n});\n```\n\n```js\n// content.js\n\nimport {Store} from 'webext-redux';\n\nconst store = new Store({\n  serializer: payload =\u003e JSON.stringify(payload, dateReplacer),\n  deserializer: payload =\u003e JSON.parse(payload, dateReviver)\n});\n```\n\nIn this example, `dateReplacer` and `dateReviver` are a custom JSON [replacer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) and [reviver](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) function, respectively. They are defined as such:\n\n```js\nfunction dateReplacer (key, value) {\n  // Put a custom flag on dates instead of relying on JSON's native\n  // stringification, which would force us to use a regex on the other end\n  return this[key] instanceof Date ? {\"_RECOVER_DATE\": this[key].getTime()} : value\n};\n\nfunction dateReviver (key, value) {\n  // Look for the custom flag and revive the date\n  return value \u0026\u0026 value[\"_RECOVER_DATE\"] ? new Date(value[\"_RECOVER_DATE\"]) : value\n};\n\nconst stringified = JSON.stringify(state, dateReplacer)\n//\"{\"todos\":[{\"id\":1,\"text\":\"Write a Web extension\",\"created\":{\"_RECOVER_DATE\":1514793600000}}]}\"\n\nJSON.parse(stringified, dateReviver)\n// {todos: [{ id: 1, text: 'Write a Web extension', created: new Date(2018, 0, 1) }]}\n```\n\n## Custom Diffing and Patching Strategies\n\nOn each state update, `webext-redux` generates a patch based on the difference between the old state and the new state. The patch is sent to each proxy store, where it is used to update the proxy store's state. This is more efficient than sending the entire state to each proxy store on every update.\nIf you find that the default patching behavior is not sufficient, you can fine-tune `webext-redux` using custom diffing and patching strategies. \n\n### Deep Diff Strategy\n\nBy default, `webext-redux` uses a shallow diffing strategy to generate patches. If the identity of any of the store's top-level keys changes, their values are patched wholesale. Most of the time, this strategy will work just fine. However, in cases where a store's state is highly nested, or where many items are stored by key under a single slice of state, it can start to affect performance. Consider, for example, the following `state`:\n\n```js\n{\n  items: {\n    \"a\": { ... },\n    \"b\": { ... },\n    \"c\": { ... },\n    \"d\": { ... },\n    // ...\n  },\n  // ...\n}\n```\n\nIf any of the individual keys under `state.items` is updated, `state.items` will become a new object (by standard Redux convention). As a result, the default diffing strategy will send then entire `state.items` object to every proxy store for patching. Since this involves serialization and deserialization of the entire object, having large objects - or many proxy stores - can create a noticeable slowdown. To mitigate this, `webext-redux` also provides a deep diffing strategy, which will traverse down the state tree until it reaches non-object values, keeping track of only the updated keys at each level of state. So, for the example above, if the object under `state.items.b` is updated, the patch will only contain those keys under `state.items.b` whose values actually changed. The deep diffing strategy can be used like so:\n\n```js\n// background.js\n\nimport {createWrapStore} from 'webext-redux';\nimport deepDiff from 'webext-redux/lib/strategies/deepDiff/diff';\n\nconst wrapStore = createWrapStore();\nconst store; // a normal Redux store\n\nwrapStore(store, {\n  diffStrategy: deepDiff\n});\n```\n\n```js\n// content.js\n\nimport {Store} from 'webext-redux';\nimport patchDeepDiff from 'webext-redux/lib/strategies/deepDiff/patch';\n\nconst store = new Store({\n  patchStrategy: patchDeepDiff\n});\n```\n\nNote that the deep diffing strategy currently diffs arrays shallowly, and patches item changes based on typed equality.\n\n#### Custom Deep Diff Strategy\n\n`webext-redux` also provides a `makeDiff` function to customize the deep diffing strategy. It takes a `shouldContinue` function, which is called during diffing just after each state tree traversal, and should return a boolean indicating whether or not to continue down the tree, or to just treat the current object as a value. It is called with the old state, the new state, and the current position in the state tree (provided as a list of keys so far). Continuing the example from above, say you wanted to treat all of the individual items under `state.items` as values, rather than traversing into each one to compare its properties:\n\n```js\n// background.js\n\nimport {createWrapStore} from 'webext-redux';\nimport makeDiff from 'webext-redux/lib/strategies/deepDiff/makeDiff';\n\nconst wrapStore = createWrapStore();\nconst store; // a normal Redux store\n\nconst shouldContinue = (oldState, newState, context) =\u003e {\n  // If we've just traversed into a key under state.items,\n  // stop traversing down the tree and treat this as a changed value.\n  if (context.length === 2 \u0026\u0026 context[0] === 'items') {\n    return false;\n  }\n  // Otherwise, continue down the tree.\n  return true;\n}\n// Make the custom deep diff using the shouldContinue function\nconst customDeepDiff = makeDiff(shouldContinue);\n\nwrapStore(store, {\n  diffStrategy: customDeepDiff // Use the custom deep diff\n});\n```\n\nNow, for each key under `state.items`, `webext-redux` will treat it as a value and patch it wholesale, rather than comparing each of its individual properties.\n\nA `shouldContinue` function of the form `(oldObj, newObj, context) =\u003e context.length === 0` is equivalent to `webext-redux`'s default shallow diffing strategy, since it will only check the top-level keys (when `context` is an empty list) and treat everything under them as changed values.\n\n### Custom `diffStrategy` and `patchStrategy` functions\n\nYou can also provide your own diffing and patching strategies, using the `diffStrategy` parameter in `wrapStore` and the `patchStrategy` parameter in `Store`, respectively. A diffing strategy should be a function that takes two arguments - the old state and the new state - and returns a patch, which can be of any form. A patch strategy is a function that takes two arguments - the old state and a patch - and returns the new state.\nWhen using a custom diffing and patching strategy, you are responsible for making sure that they function as expected; that is, that `patchStrategy(oldState, diffStrategy(oldState, newState))` is equal to `newState`.\n\nAside from being able to fine-tune `webext-redux`'s performance, custom diffing and patching strategies allow you to use `webext-redux` with Redux stores whose states are not vanilla Javascript objects. For example, you could implement diffing and patching strategies - along with corresponding custom serialization and deserialization functions - that allow you to handle [Immutable.js](https://github.com/facebook/immutable-js) collections.\n\n## Docs\n\n* [Introduction](https://github.com/tshaddix/webext-redux/wiki/Introduction)\n* [Getting Started](https://github.com/tshaddix/webext-redux/wiki/Getting-Started)\n* [Advanced Usage](https://github.com/tshaddix/webext-redux/wiki/Advanced-Usage)\n\n## Who's using this?\n\n[![Loom][loom-image]][loom-url]\n\n[![GoGuardian][goguardian-image]][goguardian-url]\n\n[![Chrome IG Story][chrome-ig-story-image]][chrome-ig-story-url]\n\n[\u003cimg src=\"https://user-images.githubusercontent.com/1683635/56149225-12f1dc00-5f7a-11e9-884c-8ee2805f10a0.png\" height=\"75\"\u003e][mabl-url]\n\n[![Storyful][storyful-image]][storyful-url]\n\nUsing `webext-redux` in your project? We'd love to hear about it! Just [open an issue](https://github.com/tshaddix/webext-redux/issues) and let us know.\n\n\n[npm-image]: https://img.shields.io/npm/v/webext-redux.svg\n[npm-url]: https://npmjs.org/package/webext-redux\n[downloads-image]: https://img.shields.io/npm/dm/webext-redux.svg\n[downloads-url]: https://npmjs.org/package/webext-redux\n[loom-image]: https://cloud.githubusercontent.com/assets/603426/22037715/28c653aa-dcad-11e6-814d-d7a418d5670f.png\n[loom-url]: https://www.useloom.com\n[goguardian-image]: https://cloud.githubusercontent.com/assets/2173532/17540959/c6749bdc-5e6f-11e6-979c-c0e0da51fc63.png\n[goguardian-url]: https://goguardian.com\n[chrome-ig-story-image]: https://user-images.githubusercontent.com/2003684/34464412-895af814-ee32-11e7-86e4-b602bf58cdbc.png\n[chrome-ig-story-url]: https://chrome.google.com/webstore/detail/chrome-ig-story/bojgejgifofondahckoaahkilneffhmf\n[mabl-url]: https://www.mabl.com\n[storyful-image]: https://user-images.githubusercontent.com/702227/140521240-be12e5ba-4f4e-4593-80a0-352f1acfe039.jpeg\n[storyful-url]: https://storyful.com\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftshaddix%2Fwebext-redux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftshaddix%2Fwebext-redux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftshaddix%2Fwebext-redux/lists"}