Skip to content

Instantly share code, notes, and snippets.

@rmadsen
Last active April 5, 2017 13:53
Show Gist options
  • Save rmadsen/dddff1f606d007b03819 to your computer and use it in GitHub Desktop.
Save rmadsen/dddff1f606d007b03819 to your computer and use it in GitHub Desktop.
Proposal for a redux-like pub-sub store

Note: I realize the below is fairly abstract, and would be happy to come up with some pseudo code if that would make things clearer. I'm still in the process of trying to piece together my thoughts. Thanks in advance for reading and for the feedback!

I have two motivations for creating a pub-sub variant of Redux, which I'll get to in a minute. I first want to describe what I mean by a "pub-sub redux variant." I'm enamored with redux's action/reducer model, but feel like there is an unnecessary limitation where components can only be connected to the root of the store state. What I'd like to propose is that instead of react-redux's mapStateToProps function, connect expects a collection of subscriptions. Each subscription is an object of the form { pathToSubscribeTo, stateToPropsHandlerFn }, where pathToSubscribeTo is a pointer/string representing a node in the state tree, and stateToPropsHandlerFn is a function that takes the updated node and returns some props that should be passed to the underlying component. Whenever the store is updated, the store will see which paths were updated and notifies only the components that registered to listen to those paths (that is, it will call each component's stateToPropsHandlerFn and merge the returned props into the underlying component—basically what connect + <Provider> currently do). I'm glossing over how the store "knows which paths were updated," but can expand on that if you're interested. I attached an example component below that illustrates this idea.

The first reason I'm interested in such a model is that I'm working on a fairly intense single page application that shows 50+ time series graphs on the same page with the following behavior: when you mouse over a graph, it shows you the value for the current "time" that you're mousing over. Simultaneously, all other graphs on the page also display the value of their data at the same point in time. This behavior lets users correlate how different graphs are related.

The Component layout for this is fairly nested. We have a structure where the <Content> contains a <GraphTable>, which encapsulates one or more <GraphGroups>, which contains many <GraphWrapper>s, which contains a <GraphLabel> and a <SparkGraph>, and this spark graph contains the <ActiveValue> label. Because ActiveValue's displayed datapoint changes rapidly on mouseovers, we end up spending a large chunk of time in the rendering code. Some of this rendering code isn't trivial, either. Based on the data that we receive, we may choose to layout the graphs in different groups, or decide not to show some of the graphs at all. All this leads to perceived lag when performing mouse overs.

One way to solve this using just redux would be to make the <ActiveValue> connected to the store and no longer have <Content> retrieve the activeValue and pass it through via props. I believed that redux prefers that you don't nest Containers inside of Components (perhaps I'm misremembering this preference, though. I can't find such a statement in the docs). If Containers in Components is in fact kosher, then it is pretty appealing to suddenly make many more items directly connected to the store, including all of the graphs and many summarization components (i.e. widgets that take in some data and display a summary). I can imagine having thousands of such components attached to the store—but this would mean that whenever the store changes, we'd have to go and notify a thousand elements, run their mapStateToProps and check if any props changed. Sure, for any given store update the vast majority of the elements would do very little work. But it still seems like an unnecessary amount of overhead for what is conceivably a rapidly changing store, which makes me believe that I'm trying to accomplish something that doesn't quite align with redux's philosophy.

The second reason I'm interested in a pub-sub system is because it lets us get relay-like semantics, where some independent logic decides when and how to fetch data. Basically, some middleware can see if there is anybody subscribing to a given path, and if so take some action to start populating that path. In my above <SparkGraph> example, I'd have each spark graph register to listen to some metric. The middleware would then use a websocket to get streaming data for that metric from a server, and continuously emit actions to update the store whenever a new datapoint arrived. Similarly I may have one or more SummarizationWidgets listening to the same data source. If no graphs or widgets that need the data are displayed, the middleware can stop subscribing for updates and garbage collect the old data in the store.

I could do a similar thing using middleware & vanilla redux right now, but there are some disadvantages. The main problem is that each component would have to dispatch(Actions.subscribeToData(...)) on mount and dispatch(Actions.unsubscribeFromData(...)) on unmount. (It may need to dispatch those actions even more frequently if the data the we're subscribing to is dependent on the component's props). This just seems like a lot of bookkeeping for each component that could be taken care of by the store. This bookkeeping, however, could only be abstracted if the store knows which components are listening to which paths.

What do you think? Can these pain points be solved in a more elegant way using current redux APIs? Does a pub-sub model have potential?

import { Component, PropTypes } from 'react';
import { connect } from 'pubsub-redux'
import {
FOO_PATH,
BAR_PATH
} from './store/paths';
class MyComponent extends Component {
render() {
return (
<div>
{`Foo is ${this.props.foo}, and bar is ${this.props.bar}`}
<span className="small">
{`There are {${this.props.numBaz}} bazzes`}
</span>
</div>
);
}
}
MyComponent.propTypes = {
foo: PropTypes.string.isRequired,
bar: PropTypes.string,
numBaz: PropTypes.number,
}
function subscribeToStore(props) {
return [
{
path: [FOO_PATH],
onLoad: (node) => ({ foo: node.foo }),
},
{
path: [BAR_PATH],
onLoad: (node, path, rootState) => {
return {
bar: node.name,
numBaz: node.baz.length,
};
}
},
];
}
export default connect(subscribeToStore)(MyComponent);
@threepointone
Copy link

This is really close to what the cljs/om.next folks are attempting as well. I'm trying to build a js clone of om.next, so far I have a query language ('paths'/'subscriptions' from above), and am able to rerender on actions. I'm working on the "knows which paths were updated" part of the problem now, as well as streaming subscriptions/remotes/etc. I'll share more once it's demo worthy, but you could just dive into om.next if you're up to it :) cheers!

@rmadsen
Copy link
Author

rmadsen commented Mar 15, 2016

Thanks @threepointone - looking forward to seeing what you come up with. I'll start prototyping this myself and we'll see what ideas we can share and swap once we both have something demo-worthy.

@markerikson
Copy link

Note that there's at least three existing Redux add-ons that try to give you more specific subscription capability: https://github.com/ashaffer/redux-subscribe , https://github.com/jprichardson/redux-watch , and https://github.com/sprightco/redux-changes . Haven't actually tried any of them, but they offer varying approaches to the concept of "watching a piece of state for changes". I realize you're going for a more specific idea of having that be hooked into something connect-like, but wanted to toss out some related prior art for reference.

@wildseansy
Copy link

Because ActiveValue's displayed datapoint changes rapidly on mouseovers

@rmadsen, perhaps your problem could be solved by throttling callbacks around the mouse move events? You could throttle exclusively by time window...or if the mouse stays within a given radius for 200ms, only then a callback is made to make any data adjustments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment