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
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
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:
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.
r node, browserify, etc
install general-store
r bower
r install general-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;
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'
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)
).
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();
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.
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);
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>
);
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
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;
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.
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
Logo design by Chelsea Bathurst