jesseskinner / hover Goto Github PK
View Code? Open in Web Editor NEWA very lightweight data store with action reducers and state change listeners.
License: MIT License
A very lightweight data store with action reducers and state change listeners.
License: MIT License
Currently the state object returned by getState and this.state is created by making a serialized copy with this:
JSON.parse(JSON.stringify(state));
Users might want to use an immutable data structure instead. They might want to allow functions or use Object.assign
to clone the state. Or any other combination of things.
To keep the API simple, we could just allow each store to define a getState()
method to implement it's own cloning (or immutable object wrapping or whatever). It might look something like this:
Hoverboard({
getState: function(state){
return JSON.parse(JSON.stringify(state));
}
});
If we do that, then the question is: should Hoverboard stop cloning the state by default with JSON serialization, and be agnostic by default with a mutable state?
Looking at geiger I saw that context are used in an interesting fashion.
https://github.com/netgusto/Geiger
There is also a blog post about react context. https://www.tildedave.com/2014/11/15/introduction-to-contexts-in-react-js.html (old)
It would be great to document how to use it similar to how the waitFor
was documented.
Here is a use case:
Assume you have a widget (component) that would want to share. Let us take twitter feed as an example. It is a pretty complex component so you would want to have a custom store for each instance of the component.
var twitterstore = Hoverboard(TwitterStore);
<Context twitterstore={twitterstore}>
<TwitterFeed />
</Context>
var TwitterFeed = React.createClass({
static contextTypes: {
twitterstore: React.PropTypes.object.isRequired
},
componentDidMount() {
this.twitterstore.getState(state => this.setState({ tweets: tweet }));
},
render() {
return this.state.tweets.map(t => <Tweet tweet={t} />);
}
};
var Tweet = React.createClass({
static contextTypes: {
twitterstore: React.PropTypes.object.isRequired
},
onRewteet(e) {
e.preventDefault();
this.context.twitterstore.Retweet(this.props.tweet.id);
},
render() {
return <div>{this.props.tweet.text} <input type="button" onClick={this.onRewteet} value="Retweet" /></div>
}
};
Assume that now the twitterstore is per user. I can now have multiple TwitterFeed component that works independently in the same page.
var userATwitterStore = Hoverboard(TwitterStore);
userATwitterStore.setUserId('A');
<Context twitterstore={userATwitterStore }>
<TwitterFeed />
</Context>
var userBTwitterStore = Hoverboard(TwitterStore);
userBTwitterStore.setUserId('B');
<Context twitterstore={userBTwitterStore }>
<TwitterFeed />
</Context>
Hi,
How would you go about handling the situation where a sub-store-action has a dependency on something else.
For example I have a logger on my base store state and I want to access it in a sub-store state how do I go about doing that?
Or am I missing something?
Cheers,
Mike
Hey,
Great job on the library, it looks really simple and clean to use.
Im a TS user and it would be awesome to get some typings. I have started to write my own but im not too good at it and having issues.
Mike
Sniffing around at flux implementations here. Hoverboard's ultra minimalist philosophy seems appealing, but onViewById/updateItem example seems like a not-great design. Would work in some cases but very confining.
Also, json serialize/deserialize seems a high to provide a read-only copy of the whole state. Could be avoided by letting users expose query functions. Advantages:
This is more of a stylistic convention than an issue with the code itself.
With a few builtin exceptions, Capitalized variable names are conventionally reserved for constructors, which Hover
technically isn't. More info: http://eslint.org/docs/rules/new-cap
I'd suggest to either use Hover
as a constructor in the documentation (const state = new Hover()
) or change it to be lowercase (const state = hover()
).
I noticed that it already works either way, but I found the documentation a bit confusing to read because of this ๐
Would it hurt to have a function like following, where State is a separate class and have helper functions on it. is this good practice.
var SimpleStore = function(path) {
function State() {
this.data = [];
this.selected = null;
}
var p = State.prototype;
p.copy = function() {
var ns = new State();
ns.data = this.data;
ns.selected = this.selected;
return ns;
}
p.findById: function(id) {
return _.find(this.data, function(item) {
return item.id === id;
});
}
var store = Hoverboard({
init: function(state) {
GET(path, function(data) {
store.data(data);
});
return new State();
},
data: function(state, data) {
var ns = state.copy();
ns.data = data;
return ns;
},
select: function(state, selected) {
var ns = state.copy();
ns.selected = selected;
return ns;
}
});
return store;
}
Creating "classes" like this is possible with Hoverboard already:
var store = Hoverboard(function(){
this.onSomething = function(){
console.log('woo');
}
});
store.something();
but the module syntax below is not yet possible, and it should be:
var store = Hoverboard(function(){
return {
onSomething: function(){
console.log('boo');
}
};
});
store.something();
It seems if you unsubscribe during a getState listener, it can cause other listeners to not be fired.
node_modules/grpc/src/node/extension_binary/node-v57-linux-x64-glibc/grpc_node.node: undefined symbol: SSL_library_init
I get tripped up forgetting to add return state
to the end of my action handlers, and it's a horrible and hard to figure out issue when suddenly part or all of your state is missing.
I propose simply checking if typeof state === 'undefined'
and not changing the state if it is undefined. To wipe the state, one can return null
or return {}
or something.
Any opinions? Concerns?
Here are some thoughts on the next breaking changes. Mostly I want to remove magic, improve predictability and hopefully stabilize the API for the long term.
Stop merging state returned from actions.
I'm starting to regret adding magic around plain objects in Hoverboard. Magic is unpredictable, and I've even surprised myself in how it works. For example, I mistakenly thought returning nothing from an action would leave the state unmodified, but actually it wipes it out. Is this a bug or expected behaviour? Since the answer is unclear, I think the functionality needs to change to be more predictable. Although it'd take a bit more work to merge new properties into state in action handlers, at least the result will be predictable - that whatever is returned from an action handler becomes the new state.
Stop making copies of plain objects for state listeners.
Although this seems like it'd help enforce immutable data, since it only works on the shallow level, it probably gives a false sense of security. It may also cause performance and memory issues when used with a large number of subscribers and large data over frequent updates. With Hoverboard.compose
, you should be able to wrap a store in some protection eg. use Object.freeze
.
It's always been challenging to combine multiple stores. The only way we could this achieve this is by adding a bunch of subscribers to each store, and then triggering actions on the main store to combine the data. I have an idea for a feature that could be built into Hoverboard to make this much easier.
For example, let's say you have a game.
var scoreStore = Hoverboard({
init: function (state, initialScore) {
return initialScore;
},
add: function (state, score) {
return state + score;
}
});
var healthStore = Hoverboard({
init: function (state, initialHealth) {
return initialHealth;
},
hit: function (state, amount) {
return state - amount;
}
});
You might want to combine these stores into a single GameStore for your game.
Currently, you'd have to do this:
var gameStore = Hoverboard({
setScore: function (state, score) {
return { score: score };
},
setHealth: function (state, health) {
return { health: health };
}
});
// subscribe to both stores
healthStore.getState(function (health) {
gameStore.setHealth(health);
});
scoreStore.getState(function (score) {
gameStore.setScore(score);
});
So this works, but what I'd like to do is make it easier to combined/compose different stores into a single store. Here's what I'm thinking:
var gameStore = Hoverboard.compose({
score: scoreStore,
health: healthStore
});
That's much simpler to understand and to write, and I think it'd make life much easier using Hoverboard.
Some other notes:
var gameStore = Hoverboard.compose({
score: scoreStore,
character: Hoverboard.compose({
health: healthStore
})
});
var asyncStore = Hoverboard.compose({
user: function (setState) {
loadUserFromServer(function (error, user) {
setState(user);
});
}
});
This module does not work with react native, triggers Unexpected end of script. I have no idea what's causing it tho :(
Hi,
Apologies for so many tickets.
This one is kind of tricky to explain.
Suppose I have an AppStore which has state { isInitted: boolean } and an "init" action.
For app store to be considered initted it must tell the AuthStore to init itself (an async operation) if that operation completes then the UserStore needs to load (async) the user based on the token that the AuthStore loaded from localStorage.
So you have a chain of async operations going on there. Normally you could handle this with promises or async await no problem but with stores its different as they dont return promises.
How would you recommend solving this chained async problem?
Mike
Hoverboard currently has two dependencies, and we could move to eliminating both of them without affecting the functionality. Hoverboard is already only 4kb minified & gzipped, but eliminating these dependencies could bring it closer to 1kb or less!
EventEmitter - being used as a PubSub for state changes, but nothing more. We could simply use an array of functions per store, and provide an unsubscribe function. This functionality is pretty basic.
Flux Dispatcher - being used to dispatch actions to stores. We don't use more than this from Dispatcher, we only need to call a specific action handler per action, so we don't even need PubSub here. We could easily enforce the rule that actions cannot be called from action handlers, which is probably the only feature in Dispatcher we're taking advantage of.
Any thoughts on this?
After using Hoverboard.compose
for a while, I've tried different ways of composing the actions. Hoverboard.compose only composes state data, and actions are ignored here. I tend to compose the actions and the state separately, eg.:
const clickCounter = Hoverboard({
add: (state=0, num) => state + num
})
export const model = Hoverboard.compose({
clickCounter
})
export const actions = {
addClick: clickCounter.add
}
This works, I guess. But it'd be nice to have the actions automatically available on model
, instead of having to pass them along. Maybe something like:
model.clickCounter.add(1)
One simple way to approach this, would be to make the stores (and other functions?) available as properties on the composed store. So that would also allow this:
const currentCount = model.clickCounter.getState()
model.clickCounter.getState(clickCount => console.log('there have been', clickCount, 'clicks'))
And if using Hoverboard.compose
to map a store's state, the original store's actions could be passed through as well, eg.
const exaggeratedClicks = Hoverboard.compose(clickCounter, clicks => clicks * 2)
exaggeratedClicks.add(5)
const clicks = exaggeratedClicks() // returns 10
And then, doing nested composition would provide a nested structure of actions, eg.
const model = Hoverboard.compose({
clicks: Hoverboard.compose({
clickCounter,
exaggeratedClicks
})
})
// passed through to original clickCounter.add() action
model.clicks.exaggeratedClicks.add(3)
model.getState() // returns {"clicks": {"clickCounter": 3, "exaggeratedClicks": 6}}
I think that'd be useful without breaking anything. The actions would map nicely to the structure of the data, by default, and you could still choose to create your own object of action mappings yourself.
Any plans to add mixins for React?
Adding these are quite painful.
componentDidMount() {
this._disposeTodoSubscription = TodoStore.getState(state => this.setState({ todos: state.todos }));
},
componentWillUnmount() {
this._disposeTodoSubscription();
}
Reflux currently has
mixins: [Reflux.connect(todoStore, "todos")]
and Reflux.ListenerMixin
mixins: [Reflux.ListenerMixin)],
onTodoChange(state) {
this.setState({ todos: state.todos });
},
componentDidMount() {
this.listenTo(todoStore, this.onTodoChange);
}
I tried to be clever with trying to avoid apply
, but just realised that arguments.length on an action handler will always be six when there are six or fewer arguments. Need to fix that.
In many Flux articles, people seem to love the fact you can drop a console.log into the Dispatcher to see everything that's happening. Since the dispatching in Hoverboard is done internally, maybe it'd be worth adding a hook to add global listeners for all actions and state changes.
Only problem is, Hoverboard has no notion of the name of a state. It only knows the name of the methods being called, the internal store instance, and the store action object. So this might be a possible implementation:
var ValueStore = Hoverboard({
onSomeAction: function(val) {
this.setState({ val: val });
}
});
// something like this would be necessary if you want to log the "name" of the store
ValueStore.name = 'ValueStore';
Hoverboard.onAction(function (store, action, args) {
console.log('[action]', store.name + '.' + action, args);
});
Hoverboard.onState(function (store, state) {
console.log('[state]', store.name, state);
});
// outputs the following to console:
// [action] ValueStore.someAction [123]
// [state] ValueStore {"val":123}
ValueStore.someAction(123);
I'm not settled on this API, just wanted to let this simmer here for a while before implementing, in case anyone has additional thoughts.
Maybe it's not even necessary, there are many other ways to debug apps. In a project I have with Hoverboard and React, I have a single render method with a console.log to see all the state entering my views, and that's been good enough so far.
After a few months in use I have some ideas of the next version of the API. Here are some of my goals and thoughts, not set in stone. Please, let me know your thoughts on any of this.
getInitialState
and just use a constructor, rehydration, or override getState
instead.this.state
, getState
and setState
, but probably will keep them.Here's a possible example of usage in v2:
class MyModel extends Hoverboard {
constructor(state) {
super(state);
// start loading async data immediately
setTimeout(() => this.addItem('async'), 1000);
}
setState(data) {
// do processing on any data passed through here (eg. add meta data, filter)
data.listLength = data.list ? list.length : 0;
return super.setState(data);
}
getState(callback) {
// could start lazily loading async data here
api.get('list/pages/count').then((pageCount) => this.setState({ pageCount });
return super.getState(callback);
}
// note this is an action and it's not called onAddItem
addItem(item) {
var list = this.state.list || [];
list.push(item);
this.setState({ list: list });
}
}
// create instance of model and pass in initial state
var model = new MyModel({ list: ['one', 'two'] });
// add a listener
var unsubscribe = model.getState(function (state) {
// do stuff with state
});
// call an action
model.addItem('new item');
Hoverboard eliminates the need for waitFor
by insisting that stores wait for each other via the getState
mechanism. However, there is nothing in place to prevent a circular dependency from forming, where Store A listens to the state of Store B, and Store B listens to the state of Store A, which can cause an endless loop. Heck, even Store A could listen to the state of Store A and cause a loop within itself.
So, we need to throw an error if the state changes within a store instance while a state change event from that store is in the middle of being emitted. In other words, only one state change event can go out per store at a time.
We already have a similar protection that an action cannot be called while an action is in progress, so this can be implemented very similarly.
Hoverboard now only has a single private global variable:
// ensure only one action is handled at a time globally
var isActionBeingHandled = 0;
I'd like to eliminate this and I think it's safe to do so. It's there to prevent actions from firing actions to other stores. I only see this as being a problem if the actions become circular (action X in store A calls action Y on store B which then calls action X on store A etc.). So, I think we can move it into each store - so that a single store can't trigger its own actions while an action is being handled, but we can allow it to call actions on other stores.
When using functions in Hoverboard.compose, the function will remain in state until setState callback is called. This doesn't happen with stores, but can happen with custom functions.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.