All Projects → HubSpot → General Store

HubSpot / General Store

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

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to General Store

mafuba
Simple state container for react apps.
Stars: ✭ 20 (-88.3%)
Mutual labels:  flux, store
Fluorine
[UNMAINTAINED] Reactive state and side effect management for React using a single stream of actions
Stars: ✭ 287 (+67.84%)
Mutual labels:  dispatcher, flux
relite
a redux-like library for managing state with simpler api
Stars: ✭ 60 (-64.91%)
Mutual labels:  flux, store
RxReduxK
Micro-framework for Redux implemented in Kotlin
Stars: ✭ 65 (-61.99%)
Mutual labels:  flux, store
Fluxxan
Fluxxan is an Android implementation of the Flux Architecture that combines concepts from both Fluxxor and Redux.
Stars: ✭ 80 (-53.22%)
Mutual labels:  dispatcher, flux
flux-redux
An application implementing Flux and Redux with few other dependencies
Stars: ✭ 24 (-85.96%)
Mutual labels:  flux, dispatcher
Verge
🟣 Verge is a very tunable state-management engine on iOS App (UIKit / SwiftUI) and built-in ORM.
Stars: ✭ 273 (+59.65%)
Mutual labels:  dispatcher, flux
Flooks
🍸 A state manager for React Hooks
Stars: ✭ 201 (+17.54%)
Mutual labels:  flux, store
Flocks.js
A radically simpler alternative to Flux - opinionated React state and rendering management
Stars: ✭ 72 (-57.89%)
Mutual labels:  data, flux
Reatom
State manager with a focus of all needs
Stars: ✭ 567 (+231.58%)
Mutual labels:  flux, store
Store
Unidirectional, transactional, operation-based Store implementation.
Stars: ✭ 477 (+178.95%)
Mutual labels:  dispatcher, store
Datastore
🐹 Bloat free and flexible interface for data store and database access.
Stars: ✭ 99 (-42.11%)
Mutual labels:  data, store
Freezer
A tree data structure that emits events on updates, even if the modification is triggered by one of the leaves, making it easier to think in a reactive way.
Stars: ✭ 1,268 (+641.52%)
Mutual labels:  flux, store
Data Store
Easily get, set and persist config data. Fast. Supports dot-notation in keys. No dependencies.
Stars: ✭ 120 (-29.82%)
Mutual labels:  data, store
Fastkv
FastKV is a real-time and high-performance persistent key-value store implemented by mmap. FastKV是由mmap实现的一个高实时性、高性能key-value持久化存储组件。
Stars: ✭ 163 (-4.68%)
Mutual labels:  store
Influxdb Client Python
InfluxDB 2.0 python client
Stars: ✭ 165 (-3.51%)
Mutual labels:  flux
Gobblin
A distributed data integration framework that simplifies common aspects of big data integration such as data ingestion, replication, organization and lifecycle management for both streaming and batch data ecosystems.
Stars: ✭ 2,006 (+1073.1%)
Mutual labels:  data
Noel
A universal, human-centric, replayable javascript event emitter.
Stars: ✭ 158 (-7.6%)
Mutual labels:  dispatcher
Onyx
Distributed, masterless, high performance, fault tolerant data processing
Stars: ✭ 2,019 (+1080.7%)
Mutual labels:  data
Kommander Ios
A lightweight, pure-Swift library for manage the task execution in different threads. Through the definition a simple but powerful concept, Kommand.
Stars: ✭ 167 (-2.34%)
Mutual labels:  dispatcher

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

# npm >= 5.0.0
npm install general-store

# yarn
yarn add general-store
// namespace import
import * as GeneralStore from 'general-store';
// or import just your module
import { define } from 'general-store';

Create a store

GeneralStore uses functions to encapsulate private data.

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

  return (
    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.

import { Dispatcher } from 'flux';
import * as GeneralStore from 'general-store';

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

var UserStore = GeneralStore.define()
  .defineGet(function() {
    return users;
  })
  .register(dispatcher);

export default UserStore;

Dispatch to the Store

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

dispatcher.dispatch({
  actionType: 'USER_ADDED', // required field
  data: {
    // 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.

var UserStoreFactory = GeneralStore.defineFactory()
  .defineName('UserStore')
  .defineGetInitialState(function() {
    return {};
  })
  .defineResponses({
    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.

describe('UserStore', () => {
  var storeInstance;
  beforeEach(() => {
    // each test will have a clean store
    storeInstance = UserStoreFactory.register(dispatcher);
  });

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

  it('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.

UserStore.get(); // returns {}
var subscription = UserStore.addOnChange(function() {
  // handle changes!
});
// addOnChange returns an object with a `remove` method.
// When you're ready to unsubscribe from a store's changes,
// simply call that method.
subscription.remove();

React

GeneralStore provides some convenience functions for supplying data to React components. Both functions rely on the concept of "dependencies" and process those dependencies to return any data kept in a Store and make it easily accessible to a React component.

Dependencies

GeneralStore has a two formats for declaring data dependencies of React components. A SimpleDependency is simply a reference to a Store instance. The value returned will be the result of Store.get(). A CompoundDependency depends on one or more stores and uses a "dereference" function that allows you to perform operations and data manipulation on the data that comes from the stores listed in the dependency:

const FriendsDependency = {
  // compound fields can depend on one or more stores
  // and specify a function to "dereference" the store's value.
  stores: [ProfileStore, UsersStore],
  deref: props => {
    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.

useStoreDependency

useStoreDependency is a React Hook that enables you to connect to a single dependency inside of a functional component. The useStoreDependency hook accepts a dependency, and optionally a map of props to pass into the deref and a dispatcher instance.

function FriendsList() {
  const friends = GeneralStore.useStoreDependency(
    FriendsDependency,
    {},
    dispatcher
  );
  return (
    <ul>
      {friends.map(friend => (
        <li>{friend.getName()}</li>
      ))}
    </ul>
  );
}

connect

The second option is a Higher-Order Component (commonly "HOC") called connect. It's similar to react-redux's connect function but it takes a DependencyMap. Note that this is different than useStoreDependency which only accepts a single Dependency, even though (as of v4) connect and useStoreDependency have the same implementation under the hood. A DependencyMap is a mapping of string keys to Dependencys:

const dependencies = {
  // simple fields can be expressed in the form `key => store`
  subject: ProfileStore,
  friends: FriendsDependency,
};

connect passes the fields defined in the DependencyMap to the enhanced component as props.

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

export default connect(
  dependencies,
  dispatcher
)(ProfileComponent);

connect also allows you to compose dependencies - the result of the entire dependency map is passed as the second argument to all deref functions. While the above syntax is simpler, if the Friends and Users data was a bit harder to calculate and each required multiple stores, the friends dependency could've been written as a composition like this:

const dependencies = {
  users: UsersStore,
  friends: {
    stores: [ProfileStore],
    deref: (props, deps) => {
      friendIds = ProfileStore.get().friendIds;
      return friendIds.map(id => deps.users[id]);
    },
  },
};

This composition makes separating dependency code and making dependencies testable much easier, since all dependency logic doesn't need to be fully self-contained.

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, the useStoreDependency hook is called inside a functional component, or a component is enhanced with connect.

var dispatcher = new Flux.Dispatcher();
GeneralStore.DispatcherInstance.set(dispatcher);

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

const users = {};

const usersStore = GeneralStore.define()
  .defineGet(() => users)
  .register(); // the dispatcher instance is set so no need to explicitly pass it

function MyComponent() {
  // no need to pass it to "useStoreDependency" or "connect" either
  const users = GeneralStore.useStoreDependency(usersStore);
  /* ... */
}

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.

type DispatcherPayload = {
  actionType: string,
  data: any,
};

type Dispatcher = {
  isDispatching: () => boolean,
  register: (handleAction: (payload: DispatcherPayload) => void) => string,
  unregister: (dispatchToken: string) => void,
  waitFor: (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

# pull in dependencies
yarn install

# run the type checker and unit tests
yarn test

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

# if all tests pass, run the dev and prod build then commit and push changes
yarn run deploy

Special Thanks

Logo design by Chelsea Bathurst

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].