Git Product home page Git Product logo

webext-redux's Introduction

WebExt Redux

A set of utilities for building Redux applications in web extensions. This package was originally named react-chrome-redux.

NPM Version NPM Downloads

Installation

This package is available on npm:

npm install webext-redux

Overview

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.

All UI Components follow the same basic flow:

  1. UI Component dispatches action to a Proxy Store.
  2. Proxy Store passes action to background script.
  3. Redux Store on the background script updates its state and sends it back to UI Component.
  4. UI Component is updated with updated state.

Architecture

Basic Usage (full docs here)

As described in the introduction, there are two pieces to a basic implementation of this package.

1. Add the Proxy Store to a UI Component, such as a popup

// popover.js

import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {Store} from 'webext-redux';

import App from './components/app/App';

const store = new Store();

// wait for the store to connect to the background page
store.ready().then(() => {
  // The store implements the same interface as Redux's store
  // so you can use tools like `react-redux` no problem!
  render(
    <Provider store={store}>
      <App/>
    </Provider>
    , document.getElementById('app'));
});

2. Wrap your Redux store in the background page with wrapStore()

// background.js

import {wrapStore} from 'webext-redux';

const store; // a normal Redux store

wrapStore(store);

That'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.

3. Optional: Apply any redux middleware to your Proxy Store with applyMiddleware()

Just 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.

// content.js
import {Store, applyMiddleware} from 'webext-redux';
import thunkMiddleware from 'redux-thunk';

// Proxy store
const store = new Store();

// Apply middleware to proxy store
const middleware = [thunkMiddleware];
const storeWithMiddleware = applyMiddleware(store, ...middleware);

// You can now dispatch a function from the proxy store
storeWithMiddleware.dispatch((dispatch, getState) => {
  // Regular dispatches will still be routed to the background
  dispatch({ type: 'start-async-action' });
  setTimeout(() => {
    dispatch({ type: 'complete-async-action' });
  }, 0);
});

4. Optional: Implement actions whose logic only happens in the background script (we call them aliases)

Sometimes 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.

// background.js

import { applyMiddleware, createStore } from 'redux';
import { alias, wrapStore } from 'webext-redux';

const aliases = {
  // this key is the name of the action to proxy, the value is the action
  // creator that gets executed when the proxied action is received in the
  // background
  'user-clicked-alias': () => {
    // this call can only be made in the background script
    browser.notifications.create(...);

  };
};

const store = createStore(rootReducer,
  applyMiddleware(
    alias(aliases)
  )
);
// content.js

import { Component } from 'react';

const store = ...; // a proxy store

class ContentApp extends Component {
  render() {
    return (
      <input type="button" onClick={ this.dispatchClickedAlias.bind(this) } />
    );
  }

  dispatchClickedAlias() {
    store.dispatch({ type: 'user-clicked-alias' });
  }
}

5. Optional: Retrieve information about the initiator of the action

There 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.

// actions.js

export const MY_ACTION = 'MY_ACTION';

export function myAction(data) {
    return {
        type: MY_ACTION,
        data: data,
    };
}
// reducer.js

import {MY_ACTION} from 'actions.js';

export function rootReducer(state = ..., action) {
    switch (action.type) {
    case MY_ACTION:
        return Object.assign({}, ...state, {
            lastTabId: action._sender.tab.id
        });
    default:
        return state;
    }
}

No changes are required to your actions, webext-redux automatically adds this information for you when you use a wrapped store.

Migrating from regular Redux

1. dispatch

Contrary to regular Redux, all dispatches are asynchronous and return a Promise. It is inevitable since proxy stores and the main store communicate via browser messaging, which is inherently asynchronous.

In pure Redux, dispatches are synchronous (which may not be true with some middlewares such as redux-thunk).

Consider this piece of code:

store.dispatch({ type: MODIFY_FOO_BAR, value: 'new value'});
console.log(store.getState().fooBar);

You can rely that console.log in the code above will display the modified value.

In webext-redux on the Proxy Store side you will need to explicitly wait for the dispatch to complete:

store.dispatch({ type: MODIFY_FOO_BAR, value: 'new value'}).then(() => 
    console.log(store.getState().fooBar)
);

or, using async/await syntax:

await store.dispatch({ type: MODIFY_FOO_BAR, value: 'new value'});
console.log(store.getState().fooBar);

2. dispatch / React component updates

This case is relatively rare.

On the Proxy Store side, React component updates with webext-redux are more likely to take place after a dispatch is started and before it completes.

While the code below might work (luckily?) in classical Redux, it does not anymore since the component has been updated before the deletePost is fully completed and post object is not accessible anymore in the promise handler:

class PostRemovePanel extends React.Component {
    (...)
    
    handleRemoveButtonClicked() {
        this.props.deletePost(this.props.post)
          .then(() => {
            this.setState({ message: `Post titled ${this.props.post.title} has just been deleted` });
          });
    }
}

On the other hand, this piece of code is safe:

    handleRemoveButtonClicked() {
        const post = this.props.post;
        this.props.deletePost(post);
          .then(() => {
            this.setState({ message: `Post titled ${post.title} has just been deleted` });
          });
        }
    }

Other

If you spot any more surprises that are worth watching out for, make sure to let us know!

Security

webext-redux supports onMessageExternal which is fired when a message is sent from another extension, app, or website. By default, if externally_connectable is not declared in your extension's manifest, all extensions or apps will be able to send messages to your extension, but no websites will be able to. You can follow this to address your needs appropriately.

Custom Serialization

You 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.

Example

For example, consider the following state in your background page:

{todos: [
    {
      id: 1,
      text: 'Write a Web extension',
      created: new Date(2018, 0, 1)
    }
]}

With no custom serialization, the state in your proxy store will look like this:

{todos: [
    {
      id: 1,
      text: 'Write a Web extension',
      created: {}
    }
]}

As 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:

// background.js

import {wrapStore} from 'webext-redux';

const store; // a normal Redux store

wrapStore(store, {
  serializer: payload => JSON.stringify(payload, dateReplacer),
  deserializer: payload => JSON.parse(payload, dateReviver)
});
// content.js

import {Store} from 'webext-redux';

const store = new Store({
  serializer: payload => JSON.stringify(payload, dateReplacer),
  deserializer: payload => JSON.parse(payload, dateReviver)
});

In this example, dateReplacer and dateReviver are a custom JSON replacer and reviver function, respectively. They are defined as such:

function dateReplacer (key, value) {
  // Put a custom flag on dates instead of relying on JSON's native
  // stringification, which would force us to use a regex on the other end
  return this[key] instanceof Date ? {"_RECOVER_DATE": this[key].getTime()} : value
};

function dateReviver (key, value) {
  // Look for the custom flag and revive the date
  return value && value["_RECOVER_DATE"] ? new Date(value["_RECOVER_DATE"]) : value
};

const stringified = JSON.stringify(state, dateReplacer)
//"{"todos":[{"id":1,"text":"Write a Web extension","created":{"_RECOVER_DATE":1514793600000}}]}"

JSON.parse(stringified, dateReviver)
// {todos: [{ id: 1, text: 'Write a Web extension', created: new Date(2018, 0, 1) }]}

Custom Diffing and Patching Strategies

On 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. If you find that the default patching behavior is not sufficient, you can fine-tune webext-redux using custom diffing and patching strategies.

Deep Diff Strategy

By 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:

{
  items: {
    "a": { ... },
    "b": { ... },
    "c": { ... },
    "d": { ... },
    // ...
  },
  // ...
}

If 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:

// background.js

import {wrapStore} from 'webext-redux';
import deepDiff from 'webext-redux/lib/strategies/deepDiff/diff';

const store; // a normal Redux store

wrapStore(store, {
  diffStrategy: deepDiff
});
// content.js

import {Store} from 'webext-redux';
import patchDeepDiff from 'webext-redux/lib/strategies/deepDiff/patch';

const store = new Store({
  patchStrategy: patchDeepDiff
});

Note that the deep diffing strategy currently diffs arrays shallowly, and patches item changes based on typed equality.

Custom Deep Diff Strategy

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:

// background.js

import {wrapStore} from 'webext-redux';
import makeDiff from 'webext-redux/lib/strategies/deepDiff/makeDiff';

const store; // a normal Redux store

const shouldContinue = (oldState, newState, context) => {
  // If we've just traversed into a key under state.items,
  // stop traversing down the tree and treat this as a changed value.
  if (context.length === 2 && context[0] === 'items') {
    return false;
  }
  // Otherwise, continue down the tree.
  return true;
}
// Make the custom deep diff using the shouldContinue function
const customDeepDiff = makeDiff(shouldContinue);

wrapStore(store, {
  diffStrategy: customDeepDiff // Use the custom deep diff
});

Now, 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.

A shouldContinue function of the form (oldObj, newObj, context) => 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.

Custom diffStrategy and patchStrategy functions

You can also provide your own diffing and patching strategies, using the diffStrategy parameter in wrapStore and the patchStrategy parameter in Store, repsectively. 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. When 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.

Aside 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 collections.

Docs

Who's using this?

Loom

GoGuardian

Chrome IG Story

Storyful

Using webext-redux in your project? We'd love to hear about it! Just open an issue and let us know.

webext-redux's People

Contributors

alecanthonygarcia avatar collectedmind avatar collinsauve avatar craigsketchley avatar davidtom avatar dbachrach avatar dependabot[bot] avatar dermeck avatar djwassink avatar durandj avatar edmundleex avatar ethanroday avatar fredericrous avatar godd9170 avatar jdolle avatar jmif avatar jvergeldedios avatar kaglowka avatar marcokam avatar msz avatar nhardy avatar paulius005 avatar sc3w avatar sidneynemzer avatar sneakypete81 avatar thetrevdev avatar tshaddix avatar vhmth avatar ymdevs avatar yuiki avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

webext-redux's Issues

promiseResponder payload clarification

I edited the wiki page under 'Advanced Usage' to clarify that the payload of the returned object is the data that actually gets resolved by the promiseResponder in the content script.

Not sure if I stated it in the clearest way etc so it might need further clarification, but it wasn't very clear to me why returning just the object I wanted wasn't resolving the promise correctly (it was coming back as undefined)

throw error if `portName` not specified

An error should probably be thrown for stupid people like myself who are tempted to specify port instead of portName in their UI components when creating a store. :-)

InitialState in create Store

Hey guys,
thanks for this amazing job of react-chrome-redux.

I have an issue by the way to initiate value to my store. The original way to do that is by the createstore method. Do i have the same possibity by new Store ?

Consider dropping "React" from package name

This package originally included react in the package name because it provided utilities for working with react components.

That was very short lived and this package no longer has anything to do with react. This was brought up in #11 .

It would be pretty straight-forward to drop react from the package name:

  • Publish current code under npm package chrome-redux
  • Rename github repo to chrome-redux
  • Add deprecation message to react-chrome-redux npm package pointing at chrome-redux for any version over 1.3.1

What do you think @vhmth ?

Security Considerations with External Messaging

I'd like us to consider the possible issues that could arise with supporting onMessageExternal. We currently don't do any sort of check on the port.sender property, meaning any other extension or website script could send messages straight to the background store.

What are the possible implications of this? Should we include something in the package to run validation on an external connection? Should we even support external messaging?

I feel like the support for external messaging was a bit premature, honestly (my bad). I feel like we could have solved #47 by simply suggesting listeners in the background page that push the messages to store.dispatch after the sender has been checked.

Maybe we can solve this by adding an option of senderIds which is an array of allowed external sender ids. We could also have an allowExternal boolean which sets up external listeners.

Store per content script/tab, synchronized with browserAction

First off, let me say I love this library. I am already using it to synchronize a store globally between all tabs and it works great!

However I have some data that is in a per-tab context, that needs to be accessible to the browserAction. I currently have some rudimentary message passing using callbacks where the browserAction does something like the following to "request" the state:

(note that I'm using Mozilla's webextension-polyfill, hence the promise-based browser functions)

async function requestData() {
    const tabQuery = {
        active: true,
        currentWindow: true
    };
    const tabs = await browser.tabs.query(tabQuery);

    const message = {
        verb: 'tabData_get',
        body: { }
    };
    const response = await browser.tabs.sendMessage(tabs[0].id, message)
    await this.receiveTabData(body);
}

The content script then responds directly to that message with the data.

I would love to be able to use the same pattern with a synchronized Redux store here. Obviously I don't expect anyone to write this for me, but was hoping for some insight as to whether or not you think this could work, and if I got it working I could submit a PR.

Here's my general thinking for a solution:

  • The wrapped store would live in the content script
  • The proxy store would live in the browserAction script
  • The background page would forward chromex.dispatch to the tab, and chromex.state to the browserAction
  • Since tabs and their stores are ephemeral, I need to avoid storing a copy of the tab state in the background script.

Resolving Confusions around Aliases

@tshaddix right now our biggest point of confusion for most folks is the separation of concerns around what an alias and an action is. This introduces a new paradigm where you have to mentally remember that aliases run in one environment and actions in another. I think it largely confuses newbie Chrome extension devs as well.

My proposal is that we figure out a way to ensure that all actions get run/dispatched on the background. I'm not even sure if there's a clean way to ensure this, but I think opening the conversation is worthwhile.

[Question] Cannot applyMiddleware redux-thunk, action undefined

Hello, I'm trying to apply redux-thunk to react-chrome-redux but I'm getting these errors when trying to dispatch an 'asynchronous' action:

Error from popup.js:
image

Error from event.js (background process)
image

When I step through after the action gets invoked, action is undefined in createStore:
image

I've been trying to debug this for a while, any help would be nice!

// Code inside React Component to test
  componentDidMount() {
    const { actions } = this.props;
    actions.asyncCount();
  }

// actions.js
export function asyncCount () {
  return function (dispatch) {
    setTimeout(function() {
      dispatch({type: "ADD_COUNT"})
    }, 1000)
  }
}

// event.js

import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

import {wrapStore} from 'react-chrome-redux';
const store = createStore(rootReducer, applyMiddleware(thunk));

wrapStore(store, {
  portName: 'DRAG_NOTE'
});

Prepare Stable Release 1.0.0

I think we're at a point to move towards a "stable" (1.X.Y) release of react-chrome-redux. We are currently "unstable" (0.X.Y) in terms of SemVer. This issue should be used to discuss any remaining public API changes which might be necessary before moving to 1.0.0.

Using with Router

Hi,

have you tried using Routing along with this?
And if so, did you wrote your own Router or used existing one (like react-router, redux-router, reacr-router-redux, etc.).

I'm looking mostly for authorization solution.

Best,
Martin

Only process dispatches for your port

If I have 2 different stores running in the background page, a dispatch to one of them will be handled by all stores - the reducers will run, the state will flow out to all connected ports, and proxy stores will fire registered subscribers.

I propose that dispatch messages include a port property, and when receiving them it will only be processed if request.port === portName.

Communicating with script injected into webpage.

Hey @tshaddix

Really impressed with this project, as I see some huge potential in the world of 'web-app augmentations'.

I'm trying to make use of react-chrome-redux through a script I've injected into a webpage from a chrome extension. I'm using https://github.com/KartikTalwar/gmail-chrome-extension-boilerplate/blob/master/main.js to get ahold of some gmail controls, and in order to do so, you have to inject your .js in to the page's header. Running plain React works great this way, but I also want a common redux store in the background. Attempting a similar workflow to your clicker-key example, is when I run into a snag:

chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument

It seems communicating to an extension is possible, and it totally stands to reason that you need to tell chrome which one... I'm just not really sure how to do that. Is this within the scope of the project/Can you point me in the right direction?

es6 & linting

My vote is for eslint since that tends to be the winner in most big projects (including internally at Loom). I can take the lead here on coming up with a boilerplate config and then pool for votes on different choices. Once we come to a consensus, I'll es6ify everything and add the linting to the testing pipeline.

Question: How to get the current tab id / info in the content script?

Hi

My scenario is that I am tracking a context menu click on selected text. I need to inform only the current tab to perform subsequent action.

I can use https://github.com/tshaddix/react-chrome-redux#4-optional-retrieve-information-about-the-initiator-of-the-action but this broadcasts the tab information to all tabs. Is there some way in react-chrome-redux to compare this or do I need to track the current tab using

chrome.tabs.onUpdated.addListener

Thanks
RV

React in library name?

It seems that the library is completely free from being dependent on react. It can be used with say angular2+redux.
Any reason why it was named so?

Objective of this project

Hey guys.
So what is the goal of this project? Given that there is Chrome Storage, which can be used to store data there, what is real benefit of using single Redux Store within background Page in comparison to multiple ones inside Views?
It seems to me that this is somewhat overhead. Why don't just treat each View as separate application with its complete structure?
I'm pretty newbie in extension development world, thus I might be missing something obvious in this regard.

Firefox webExt

Hi ! Still didn't have time to try this, but since Firefox now support WebExtension could this be used as it is in as a firefox extension ?

[Question] How to use this with react-router or react-router-redux?

Hi,

I've setup an action to login a user after the login form was submitted. I've created a matching alias to have this async action of checking credentials happen in the background page. However, I'm using react-router and would like to "redirect" on a successful login. This happens by doing:

import { browserHistory } from 'react-router';

browserHistory.push('<new-path-here>');

As you might expect, having to setup the routing in the popup page (together with the browserHistory) we can't just access the same browserHistory object in the background page. Meaning that one cannot simply redirect a user after a successful login. See code example:

// popup

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Store } from 'react-chrome-redux';
import { Router, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import getRoutes from './routes';

const store = new Store({
    portName: 'xxx'
});

const unsubscribe = store.subscribe(() => {
    unsubscribe();
    const history = syncHistoryWithStore(browserHistory, store);

    ReactDOM.render(
        <Provider store={store}>
            <Router history={history}>
                {getRoutes(store)}
            </Router>
        </Provider>,
        document.querySelector('#app')
    );
});
// background

import firebase from 'firebase';
import { browserHistory } from 'react-router';

// This is an aliased action executing from the background
// with redux-thunk
const loginUser = () => {
    return (dispatch) => {
        firebase.auth().signInWithEmailAndPassword(email, password)
            .then(user => {
                dispatch({
                    type: LOGIN_SUCCESS,
                    payload: user
                });

                // Redirect here
                // This won't work, because the browserHistory obviously differs
                // from the one in the popup page.
                browserHistory.push('/dashboard');
        });
    };
};

I hope this is kind of clear? My question however is: is there a way this issue is supported by react-chrome-redux and am I just not seeing it? Or should I use messaging to send a message to my popup page saying: "Hey, all is fine, you can redirect now using the correct browserHistory object"?

Note: don't mind missing imports and such, just assume everything works. I just want to figure out, how I can get back to the browserHistory in a neat way.

Thanks in advance!

[Question] Could you provide more detail about alias and dispatchResponder

Hi,

I originally using redux-thunk for async action, for example:

// actions/index.js
function requestData() {
  return (dispatch, getState) => {
    dispatch({
      type: 'REQUEST_DATA',
      ...
    });

    superAgent
      .get(...)
      .end((err, res) => {
        if (err) {
          dispatch({
            type: 'REQUEST_DATA_FAIL',
            ...
          });
        } else {
          dispatch({
            type: 'REQUEST_DATA_SUCCESS',
            ...
          });
        }
      });
}

How can I change to use alias & dispatchResponder ?

I think I need to wrap superAgent to an alias action for REQUEST_DATA, but I don't know how to modify my action and how to use reduxPromiseResponder when remote api response..

Thanks.

support for non-immutable store?

Hi,
So we have a store that's totally non-immutable. We are using parse-server, so our store actually contains many parse object, which are essentially an object with function that cannot be serialize..
As my understanding of this module, it works by sending the store update with json messages. Upon sending these messages I get some errors which (I think) are a result of trying to form a JSON object from these objects which are not meant for that.

My question is basically if anyone has encountered such behaviour, or if you are aware of a workaround.

I cannot afford to change my store to be immutable, as this will require too much unwanted changes with the entire logic of the original web app - which we wanted to reuse its components.

Another option would be to fork this repo and change the messaging logic, but I'm not sure if even then it would be possible.

Would love to hear your insights about this one!

Cheers,
IY

State not Updating Properly after Page Refresh?

What's up Tyler. This issue may be due to my limited understanding exactly how react-redux should work, but here's what's happening:

  1. Create recording.
  2. UI gets updated.
  3. End recording.
  4. UI gets updated to be removed.

This is the proper behavior for our extension. When I do this:

  1. Create recording.
  2. UI gets updated.
  3. Refresh page.
  4. UI comes back to proper state.
  5. End recording.
  6. UI is not removed.

The UI is not removed and in fact a nested component that shouldn't be getting rendered does and its mapStateToProps function is getting called (which is throwing an error). I made a recording showcasing the bug with our tool:

https://www.opentest.co/share/d07eab70645c11e69680a3b55516ed77

Not sure what could be the cause, but I'm game to look into this today since this is kinda a high priority for us to fix.

Question: Comparison w/ similar projects

Thanks for this project & the video presentation you did. I'm new to chrome extensions, so your level of documentation/explanation is great--def. best I've found on a project like this.

But on that note I was wondering: can you talk a bit about your motivation for creating this project vice using one of the small # of similar projects? e.g. crossbuilder w/ 'cross-messaging' ?

Asking b/c as a newbie, it's tough to differentiate & wanna make sure i understand which tools fit which projects.

README Enhancement: Initial store state is by default an empty object

Something that isn't immediately obvious from the README is that the proxy Store() by default, will initialise the state as an empty object. This causes issues with react-redux if you are accessing nested properties in the state.

Trawling through the code, however, https://github.com/tshaddix/react-chrome-redux/blob/b29d0e9b352bea1ac999415a73f0521eb63327f3/src/store/Store.js#L9 shows that there is an option to initialise a default state before this proxy Store receives state information from the wrapped Store. This is practical for small projects, and should be added to the README.

For larger, more complex initial states though, it may be more practical to wait until the proxy Store receives its initial state. See example below:

const unsubscribe = store.subscribe(() => {
   unsubscribe();
   render(
    <Provider store={store}>
      <App/>
    </Provider>
    , document.getElementById('app'));
});

This example ensures that the App is only rendered upon receiving the initial state from the wrapped store. I think this would also be worth adding to the README.

Unable to use with Apollo Client

Hello there, I tried using this package with Apollo Client but it's not working. Apollo Client was able to fetch results from graphql server but couldn't dispatch actions to update the store with the fetched result. I have been investigating this for a few hours now but still couldn't find the problem. Any ideas on what the problem could be? Thanks!

Edit:

I was able to dispatch actions from content scripts. So, by right, Apollo Client should be able to do the same.

Can react-chrome-redux be used for apps?

Hi All,

Apologies if this is a dumb question as I'm new to Google App/Extension development. I'm trying to create a kiosk application to run on a Chromebit. Can react-chrome-redux be used for apps rather extensions? With my limited knowledge there appears to be some differences between the two. My application currently is a simple React Redux app that uses Redux Thunk to fetch data from a rest api.

If this is possible I'd really appreciate some pointers.

Many thanks in advance.

Make dispatch and getState available in alias

I might be using a bad practice, but I feel like there is a need for making dispatch and getState available in alias.

Reason being if I want to make remote API calls in the background page, and then dispatch an action when the response comes back, it seems I would be out of luck. I am thinking of using alias being kind of like using a thunk, where I can have an impure function that also dispatches other actions, and have access to the state.

I see that the store is already available in the closure. Any reason you didn't make that available?

Empty state object passed on first call

Not sure if this is an issue, a gotcha, the fact that I'm using the library wrongly or just the way react works (still new to this) - but I've noticed that when sending state from my event page to either the popup or content script that on the first call to mapStateToProps() that the state object is empty so my initial render function often get's "can't access ... of undefined" errors.

I got round the issue by having mapStateToProps return suitable defaults if the expected fields were missing.

Just thought I'd mention it.

AND...

Then I found the same point mentioned in the closed issues (#52) - #52 and the fix mentioned there also worked for me.

Feel free to close this issue if needs be :)

Content Store is not getting a response value from sendMessage

I noticed that for some actions that get dispatched via mapDispatchToProps, I'm getting the following error:

TypeError: Cannot read property 'payload' of undefined

This error pops up here. I'm opening this in case anyone could possibly think what the issue may be, but I'm investigating.

Note: I'm using redux-thunk and frequently dispatch functions instead of action values with a type, but that shouldn't be an issue. I just thought I'd leave that note here in case it may be useful to debug.

aliases middleware need to be first?

We've tried to implement this part of the docs (advances usage)

import {alias} from 'react-chrome-redux';

import aliases from '../aliases';

const middleware = [
  alias(aliases),
  // whatever middleware you want (like redux-thunk)
];

// createStoreWithMiddleware... you know the drill

We've had some trouble with our promise-middleware not firing after using the aliases, and we've found out that the aliases middleware needs to be the first in the middleware chain.
I think this needs to be more clear in the docs.

Do you guys agree?

How to pass functions in payload?

Hello there, I'm trying to pass a function to payload and retrieve it in the middleware. But, it seems like the function that I pass in gets stripped off. I don't see anything in the src that strips functions off the payload. Any ideas how I could solve this? Thanks!

How easy is it to use this library for Chrome extensions?

As of now, playing few days with this library I've found it a bit tricky to use.

1. Let's assume I want to inject a button somewhere on the page:

  • original DOM
<nav class='menu'>
    <a href="?">First</a>
    <a href="?">Second</a>
    <a href="?">Third</a>
</nav>
  • element to be injected
<a href="?">Fourth</a>

because of the way React works, it's not possible to simply render my own component inside nav.menu because it's going to replace everything inside. So what I would have to do is to wrap my component in a new DOM element:

render(<MenuItem />, document.getElementById('item-anchor'))

<nav class='menu'>
    <a href="?">First</a>
    <a href="?">Second</a>
    <a href="?">Third</a>
    <span id="item-anchor">
        <a href="?">Fourth</a>
    </span>
</nav>

2. How to render multiple instances of the same component with some data from an API?

  • original DOM
<ul>
    <li>
        <span>Text</text> // the text of this item should be replaced with some data from ajax
    </li>
    <li>
        <span>Text</text> // the text of this item should be replaced with some data from ajax
    </li>
    <li>
        <span>Text</text> // the text of this item should be replaced with some data from ajax
    </li>
</ul>

let's say I create a react component for this:

// TextComponent
... 
render () {
    return (
        <span>{this.props.text}</span>
    )
}

how to actually render my above component multiple times and by passing in the text prop returned from the API?

I have the index.js file that does
render(<TextComponent text={this.store.response[i]}/>, ); I can't iterate in the component because the items to be rendered are not siblings, nor I can iterate in index.js and call render() multiple times and passing in the text from an array.

In my opinion it works OK for apps an popups, because you are in full control of the DOM you create and therefore you can encapsulate everything nicely. However, with injecting content into the page, where you have to inject multiple elements here and there, it makes it too difficult to handle in my opinion, but I hope I'm wrong on this, so please ... any thoughts would be much appreciated.

Why am I having trouble with setting initialState to an object?

I used this example to setup my extension using react-chrome-redux: https://github.com/tshaddix/react-chrome-redux-examples

Currently in the countReducer, it has:
const initialState = 0;

Everything works fine.

But if I update that to
const initialState = { counter: 0 };

Then update mapdispatchtoProps from

count: state.count

to

count: state.count.counter

I get an error that says can't find property counter of undefined.

If I setup the initial state as values (not objects), everything works fine.

What am I not understanding?

Thanks.

Support for WebExtensions?

Browsers seem to be standardizing their extensions API with WebExtensions

Does react-chrome-redux work with WebExtensions out of the box or will some modifications have to be made for this to work?

[Question] Components not receiving the props

Hello!

I'm a former middle school teacher who recently got interested in programming, and I'm currently trying to build a chrome extension, using react-redux, as one of my first projects. I stumbled across react-chrome-redux, and it was exactly what I was looking for!

Unfortunately, I've unable to correctly set it up. I watched the live-code demo and read through all to see what I was missing, but I haven't been able to figure it out.

I tried to first just replicate the clicker example from the video, so my UI component and background.js basically looks exactly the same as the demo:

import React from 'react';
import {render} from 'react-dom';

import App from './lib/components/app';

import {Store} from 'react-chrome-redux';
import {Provider} from 'react-redux';

const proxyStore = new Store({
  portName: 'example'
});

document.addEventListener('DOMContentLoaded', () => {
  render(
    <Provider store={proxyStore}><App /></Provider>
    , document.getElementById('root'));
});
import React, {Component} from 'react';
import {connect} from 'react-redux';

class App extends Component {
  constructor(props) {
    super(props);
  }


  componentDidMount() {
    document.addEventListener('click', () => {
      this.props.dispatch({
        type: 'ADD_COUNT'
      });
    });
  }

  render() {
    return (
      <div>
        Click Count: {this.props.count}
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    count: state.count
  };
};

export default connect(mapStateToProps)(App);

For some reason, however, the UI component does not seem to actually be receiving the correct props, and when I do click, this error message pops up:

image

This is a portion of my manifest.json:

  "version": "1.0",
  "background": {
     "scripts": [
       "background.js"
     ],
     "persistent": true
   },
  "browser_action": {
   "default_icon": "icon.png",
   "default_popup": "popup.html"
  }

Please let me me know if this wasn't the correct place to ask for this type of help, or if any more specific code needed to be provided.

Any help would be greatly appreciated. Thanks so much!

Performance considerations

I'm checking this library as architecture option for our chrome extension. Conceptually, your work looks fantastic.
I just wonder if it provides adequate performance?

If I get it right, given an background page and a content script, both communicate using chrome's port.postMessage. What happens is that on each state change, the whole store's state being transferred from background to content script.

Here's excerpt from wrapStore.js that shows the (possible) performance bottleneck:

const sendState = () => {
  port.postMessage({
    type: STATE_TYPE,
    payload: store.getState()
  });
};

AFAIK port.postMessage serializes data it sends. So as state grows and as number of small state manipulations is large, the performance hit of serialization should be noticeable.

Another and even bigger issue is that each time state changes, it will be fully re-created in content script, including parts of the state that weren't modified. This will cause re-render of all components. Normally, not modified parts will be kept in same place in memory, so only changed parts of the state will re-render.

So the question is: do you experience any performance issues in extension that uses react-chrome-redux?

If you do, maybe using some diff patching algorithm like jsondiffpatch, and passing over port only the change delta can in theory improve the performance.

What do you think?

State is undefined

I have a simple reducer as following:

import * as ActionTypes from '../../shared/ActionTypes'

const initialState = {
  authorized : 0
}

export default (state = initialState, action) => {
  switch (action.type) {
    case ActionTypes.LOGIN:
      return {
        authorized : 1
      }
    case ActionTypes.LOGOUT:
      return  {
        authorized : 0
      }
    default:
      return state;
  }
};

When I try to access authorized from my popup, it throws an error: authorized is undefined. However, if I changed the initial state to const initialState = 0, it works.

This does not simply work: <h1>{this.props.auth.authorized}</h1>.
If I remove this line and try to print this.props.auth.authorized inside render(), I see that it is undefined first, then 0.
I guess the state is not initialized properly?

Full code : https://github.com/onurhb/Test

Persistent background pages deprecated

Love the concept, but I was reading in the Chrome extension docs that persistent background pages are deprecated in favor of event pages ("persistent": false). Under Best practices when using event pages it says:

  1. If you need to keep runtime state in memory throughout a browser session, use the storage API or IndexedDB. Since the event page does not stay loaded for long, you can no longer rely on global variables for runtime state.

Just wondering what your thoughts are since I believe react-chrome-redux keeps the state in background. Would you still recommend using react-chrome-redux on a new project right now? Or do you think it would be somewhat easy (or even possible) to change it to a non-persistent event page that kept state in local storage?

Lag in Connect Time for Proxy Store?

I have a minimal project running where there are a couple of reducers with default values running in the background. I use wrapStore immediately in the background script and immediately set up a proxy store via the Store constructor in my popup window. I noticed that this leads to an empty object state in one of my container components (when mapeStateToProps is called on it). If I set a 100ms timeout before rendering my <PopupApp> surrounded by <Provider> with the proxy store set to it, everything works.

Seems like there may be a race condition? Should I be listening for an event so I know the proxy store has established its connection correctly? Seems like onConnect is a good candidate and the proxy store object should emit some sort of event when the connection has been made and the initial state is filled in?

Is immutable.js compatible with this library?

Immutable.js does not seem to work with this setup. I am always getting normal objects in my selectors instead of immutable states. More chances are that I am doing something wrong, but just wanted to confirm. Please close this issue if it's compatible.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.