- Notifications
You must be signed in to change notification settings - Fork27
Simple, flexible store implementation for Flux. #hubspot-open-source
License
HubSpot/general-store
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
general-store aims to provide all the features of aFlux store without prescribing the implementation of that store's data or mutations.
Briefly, a store:
- contains any arbitrary value
- exposes that value via a get method
- responds to specific events from the dispatcher
- notifies subscribers when its value changes
That's it. All other features, like Immutability, data fetching, undo, etc. are implementation details.
Read more about thegeneral-store rationaleon the HubSpot Product Team Blog.
# npm >= 5.0.0npm install general-store# yarnyarn add general-store
// namespace importimport*asGeneralStorefrom'general-store';// or import just your moduleimport{define}from'general-store';
GeneralStore uses functions to encapsulate private data.
vardispatcher=newFlux.Dispatcher();functiondefineUserStore(){// data is stored privately inside the store module's closurevarusers={123:{id:123,name:'Mary',},};return(GeneralStore.define().defineName('UserStore')// the store's getter should return the public subset of its data.defineGet(function(){returnusers;})// handle actions received from the dispatcher.defineResponseTo('USER_ADDED',function(user){users[user.id]=user;}).defineResponseTo('USER_REMOVED',function(user){deleteusers[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 ofregister from a module.
import{Dispatcher}from'flux';import*asGeneralStorefrom'general-store';vardispatcher=newDispatcher();varusers={};varUserStore=GeneralStore.define().defineGet(function(){returnusers;}).register(dispatcher);exportdefaultUserStore;
Sending a message to your stores via the dispatcher is easy.
dispatcher.dispatch({actionType:'USER_ADDED',// required fielddata:{// optional field, passed to the store's responseid:12314,name:'Colby Rabideau',},});
The classic singleton store API is great, but can be hard to test.defineFactory() provides an composable alternative todefine() that makestesting easier and allows you to extend store behavior.
varUserStoreFactory=GeneralStore.defineFactory().defineName('UserStore').defineGetInitialState(function(){return{};}).defineResponses({USER_ADDED:function(state,user){state[user.id]=user;returnstate;},USER_REMOVED:function(state,user){deletestate[user.id];returnstate;},});
Like singletons, factories have a register method. Unlike singletons, thatregister method can be called many times and will always return anewinstance of the store described by the factory, which is useful in unit tests.
describe('UserStore',()=>{varstoreInstance;beforeEach(()=>{// each test will have a clean storestoreInstance=UserStoreFactory.register(dispatcher);});it('adds users',()=>{varmockUser={id:1,name:'Joe'};dispatcher.dispatch({actionType:USER_ADDED,data:mockUser});expect(storeInstance.get()).toEqual({1:mockUser});});it('removes users',()=>{varmockUser={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, theInspectStore 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.
UserStore.get();// returns {}varsubscription=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();
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 aStore and make it easily accessible to a React component.
GeneralStore has a two formats for declaring data dependencies of React components. ASimpleDependency is simply a reference to aStore instance. The value returned will be the result ofStore.get(). ACompoundDependency 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 thestores listed in the dependency:
constFriendsDependency={// 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();returnfriendIds.map(id=>users[id]);},};
Once you declare your dependencies there are two ways to connect them to a react component.
useStoreDependency is aReact Hook that enables you to connect to a single dependency inside of a functional component. TheuseStoreDependency hook accepts a dependency, and optionally a map of props to pass into thederef and a dispatcher instance.
functionFriendsList(){constfriends=GeneralStore.useStoreDependency(FriendsDependency,{},dispatcher);return(<ul>{friends.map(friend=>(<li>{friend.getName()}</li>))}</ul>);}
The second option is a Higher-Order Component (commonly "HOC") calledconnect. It's similar toreact-redux'sconnect function but it takes aDependencyMap. Note that this is different thanuseStoreDependency which only accepts a singleDependency, even though (as of v4)connect anduseStoreDependency have the same implementation under the hood. ADependencyMap is a mapping of string keys toDependencys:
constdependencies={// simple fields can be expressed in the form `key => store`subject:ProfileStore,friends:FriendsDependency,};
connect passes the fields defined in theDependencyMap to the enhanced component as props.
// ProfileContainer.jsfunctionProfileContainer({ 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>);}exportdefaultconnect(dependencies,dispatcher)(ProfileComponent);
connect also allows you to compose dependencies - the result of the entire dependency map is passed as the second argument to allderef 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:
constdependencies={users:UsersStore,friends:{stores:[ProfileStore],deref:(props,deps)=>{friendIds=ProfileStore.get().friendIds;returnfriendIds.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.
The common Flux architecture has a single central dispatcher. As a convenienceGeneralStore allows you to set a global dispatcher which will become the default when a store is registered, theuseStoreDependency hook is called inside a functional component, or a component is enhanced withconnect.
vardispatcher=newFlux.Dispatcher();GeneralStore.DispatcherInstance.set(dispatcher);
Now you can register a store without explicitly passing a dispatcher:
constusers={};constusersStore=GeneralStore.define().defineGet(()=>users).register();// the dispatcher instance is set so no need to explicitly pass itfunctionMyComponent(){// no need to pass it to "useStoreDependency" or "connect" eitherconstusers=GeneralStore.useStoreDependency(usersStore);/* ... */}
At HubSpot we use theFacebook Dispatcher, but any object that conforms to the same interface (i.e. has register and unregister methods) should work just fine.
typeDispatcherPayload={actionType:string,data:any,};typeDispatcher={isDispatching:()=>boolean,register:(handleAction:(payload:DispatcherPayload)=>void)=>string,unregister:(dispatchToken:string)=>void,waitFor:(dispatchTokens:Array<string>)=>void,};
UsingRedux 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 thedefineFactory 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
# pull in dependenciesyarn install# run the type checker and unit testsyarn test# if all tests pass, run the dev and prod buildyarn run build-and-test# if all tests pass, run the dev and prod build then commit and push changesyarn run deployLogo design byChelsea Bathurst
About
Simple, flexible store implementation for Flux. #hubspot-open-source
Topics
Resources
License
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors13
Uh oh!
There was an error while loading.Please reload this page.
