HubSpot/general-store

Name: general-store

Owner: HubSpot

Description: Simple, flexible store implementation for Flux. #hubspot-open-source

Created: 2015-02-03 16:39:19.0

Updated: 2018-05-02 08:49:31.0

Pushed: 2017-12-04 18:13:45.0

Homepage: http://github.hubspot.com/general-store

Size: 581

Language: JavaScript

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

HubSpot/general-store

NPM version Build Status

general-store aims to provide all the features of a Flux store without prescribing the implementation of that store's data or mutations.

Briefly, a store:

  1. contains any arbitrary value
  2. exposes that value via a get method
  3. responds to specific events from the dispatcher
  4. notifies subscribers when its value changes

That's it. All other features, like Immutability, data fetching, undo, etc. are implementation details.

Read more about the general-store rationale on the HubSpot Product Team Blog.

Install
r node, browserify, etc
install general-store

r bower
r install general-store
Create a store

GeneralStore uses functions to encapsulate private data.

dispatcher = new Flux.Dispatcher();
tion defineUserStore() {
 data is stored privately inside the store module's closure
r users = {
123: {
  id: 123,
  name: 'Mary'
}


turn GeneralStore.define()
.defineName('UserStore')
// the store's getter should return the public subset of its data
.defineGet(function() {
  return users;
})
// handle actions received from the dispatcher
.defineResponseTo('USER_ADDED', function(user) {
  users[user.id] = user;
})
.defineResponseTo('USER_REMOVED', function(user) {
  delete users[user.id];
})
// after a store is "registered" its action handlers are bound
// to the dispatcher
.register(dispatcher);

If you use a singleton pattern for stores, simply use the result of register from a module.

Dispatcher = require('flux').Dispatcher;
GeneralStore = require('general-store.js');

dispatcher = new Dispatcher();
users = {};

UserStore = GeneralStore.define()
efineGet(function() {
return users;

egister(dispatcher);

le.exports = UserStore;
Dispatch to the Store

Sending a message to your stores via the dispatcher is easy.

atcher.dispatch({
tionType: 'USER_ADDED', // required field
ta: { // optional field, passed to the store's response
id: 12314,
name: 'Colby Rabideau'


Store Factories

The classic singleton store API is great, but can be hard to test. defineFactory() provides an composable alternative to define() that makes testing easier and allows you to extend store behavior.

UserStoreFactory = GeneralStore.defineFactory()
efineName('UserStore')
efineGetInitialState(function() {
return {};

efineResponses({
'USER_ADDED': function(state, user) {
  state[user.id] = user;
  return state;
},
'USER_REMOVED': function(state, user) {
  delete state[user.id];
  return state;
},
;

Like singletons, factories have a register method. Unlike singletons, that register method can be called many times and will always return a new instance of the store described by the factory, which is useful in unit tests.

ribe('UserStore', () => {
r storeInstance;
foreEach(() => {
// each test will have a clean store
storeInstance = UserStoreFactory.register(dispatcher);
;

('adds users', () => {
var mockUser = {id: 1, name: 'Joe'};
dispatcher.dispatch({actionType: USER_ADDED, data: mockUser});
expect(storeInstance.get()).toEqual({1: mockUser});
;

('removes users', () => {
var mockUser = {id: 1, name: 'Joe'};
dispatcher.dispatch({actionType: USER_ADDED, data: mockUser});
dispatcher.dispatch({actionType: USER_REMOVED, data: mockUser});
expect(storeInstance.get()).toEqual({});
;

To further assist with testing, the InspectStore module allows you to read the internal fields of a store instance (e.g. InspectStore.getState(store)).

Using the Store API

A registered Store provides methods for “getting” its value and subscribing to changes to that value.

Store.get() // returns {}
subscription = UserStore.addOnChange(function() {
 handle changes!

ddOnChange returns an object with a `remove` method.
hen you're ready to unsubscribe from a store's changes,
imply call that method.
cription.remove();
React
DependencyMap

GeneralStore has a simple format for declaring dependencies.

t dependencies = {
 simple fields can be expressed in the form `key => store`
bject: ProfileStore,
 compound fields can depend on one or more stores
 and specify a function to "dereference" the store's value
iends: {
stores: [ProfileStore, UsersStore],
deref: (props, state) => {
  friendIds = ProfileStore.get().friendIds;
  users = UsersStore.get();
  return friendIds.map(id => users[id]);
}


Once you declare your dependencies there are two ways to connect them to a react component.

connect

GeneralStore provides a component “enhancer” called connect. It's similar to redux's connect function but it takes a general store DependencyMap. connect passes the fields defined in the DependencyMap to the enhanced component as props.

rofileContainer.js
tion ProfileContainer({friends, subject}) {
turn (
<div>
  <h1>{subject.name}</h1>
  {this.renderFriends()}
  <h3>Friends</h3>
  <ul>
    {Object.keys(friends).map(id => <li>{friends[id].name}</li>)}
  </ul>
</div>



rt default connect(dependencies, dispatcher)(ProfileComponent);
StoreDependencyMixin

If you use React.createClass, GeneralStore also provides a mixin. Instead of passing the dependency fields to the component as props, StoreDependencyMixin exposes dependency data in component local state.

ProfileComponent = React.createClass({
xins: [
GeneralStore.StoreDependencyMixin(dependencies, dispatcher)


nder: function() {
return (
  <div>
    <h1>{this.state.subject.name}</h1>
    <h3>Friends</h3>
    <ul>
      {Object.keys(this.state.friends).map((id) => (
        <li>{friends[id].name}</li>
      ))}
    </ul>
  </div>
);


Default Dispatcher Instance

The common Flux architecture has a single central dispatcher. As a convenience GeneralStore allows you to set a global dispatcher which will become the default when a store is registered, a component is enhanced with connected, or a StoreDependencyMixin is created.

dispatcher = new Flux.Dispatcher();
ralStore.DispatcherInstance.set(dispatcher);

Now you can register a store without explicitly passing a dispatcher:

users = {};

ralStore.define()
efineGet(() => users)
egister(); // the dispatcher instance is set so no need to explicitly pass it
Dispatcher Interface

At HubSpot we use the Facebook Dispatcher, but any object that conforms to the same interface (i.e. has register and unregister methods) should work just fine.

 DispatcherPayload = {
tionType: string;
ta: any;


 Dispatcher = {
Dispatching: () => bool;
gister: (
handleAction: (payload: DispatcherPayload) => void
=> string;
register: (dispatchToken: string) => void;
itFor: (dispatchTokens: Array<string>) => void;

Redux Devtools Extension

Using Redux devtools extension you can inspect the state of a store and see how the state changes between dispatches. The “Jump” (ability to change store state to what it was after a specific dispatch) feature should work but it is dependent on you using regular JS objects as the backing state.

Using the defineFactory way of creating stores is highly recommended for this integration as you can define a name for your store and always for the state of the store to be inspected programmatically.

Build and test

Install Dependencies

ll in dependencies
install

n the type checker and unit tests
test

 all tests pass, run the dev and prod build
run build-and-test

 all tests pass, run the dev and prod build then commit and push changes
run deploy
Special Thanks

Logo design by Chelsea Bathurst


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.