3.0.0-alpha.3 • Published 8 years ago

hoverboard v3.0.0-alpha.3

Weekly downloads
31
License
MIT
Repository
github
Last release
8 years ago

Hoverboard

A very lightweight (anti-gravity?) data model and Flux store with actions and a state change listener.

NPM version Downloads

Build Status Coverage Status Dependency status Dev Dependency status

Installation

You can use npm to install Hoverboard, or download the raw file here.

For more information, check out the Concept, Usage, Documentation and FAQ below.

npm install hoverboard

Concept

Hoverboard greatly simplifies Flux while staying true to the concept.

The basic usage of Hoverboard is:

var store = Hoverboard({
	action: function (state, value) {
		return { value: value };
	}
});

store.action("Hello, World!");

Hoverboard makes it incredibly easy to subscribe to state changes. You can add a listener by calling the model as a function. Your callback will be called immediately at first, and again whenever the state changes. This works great with React.js, because you can easily re-render your component whenever the store changes its state.

store(function (props) {
	ReactDOM.render(
		<UserProfile {...props} actions={store} />,
		document.getElementById('user-profile')
	);
});

Hoverboard was inspired by other Flux implementations, like Redux, Alt and Reflux. Those versions are very lightweight, but Hoverboard is practically weightless.

Usage

Here's how you might use Hoverboard to keep track of clicks with a ClickCounter.

var ClickCounter = Hoverboard({
	click: function(state, text) {
		return {
			value: state.value + 1,
			log: state.log.concat(text)
		};
	},
	reset: function() {
		// go back to defaults
		return {
			value: 0,
			log: []
		};
	}
});

// initialize with defaults
ClickCounter.reset();

// listen to changes to the state
var unsubscribe = ClickCounter(function (clickState) {
	document.write(JSON.stringify(clickState) + "<br>");
});

ClickCounter.click('first');
ClickCounter.click('second');

// reset back to empty state
ClickCounter.reset();

unsubscribe();

ClickCounter.click("This won't show up");

If you run this example, you'll see this:

{"value":0,"log":[]}
{"value":1,"log":["first"]}
{"value":2,"log":["first","second"]}
{"value":0,"log":[]}

To see how Hoverboard fits into a larger app, with ReactJS and a router, check out the Hoverboard TodoMVC.

Documentation

Hoverboard is a function that takes an actions object and returns a store object.

Syntax

store = Hoverboard(actions[, initialState]);

actions object

  • Any properties of the actions object will be exposed as methods on the returned store object.
  • If your state is a plain object, and you return plain objects from your actions, they will be shallow merged together.
  • Note that your actions will automatically receive state as the first parameter, followed by the arguments you pass in when calling it.

    	```javascript
    	store = Hoverboard({
    		hideItem: function(state, id) {
    			var items = state.items;
    
    			items[id].hidden = true;
    
    			// return the new state
    			return { items: items };
    		},
    
    		items: function(state, items) {
    			return { items: items };
    		},
    
    		error: function(state, error) {
    			return { error: error };
    		}
    	});
    
    	// you could load the store contents asynchronously and pass in as action
    	api.getItems(function (error, items) {
    		if (error) {
    			return store.error(error);
    		}
    
    		store.items(items);
    	});
    
    	// later
    	store.hideItem("abc");
    	```

Return value

Hoverboard(actions[, initialState]) will return a store object.

store object methods
  • store()

    	- Returns the store's current state.
  • unsubscribe = store(function)

    	- Adds a listener to the state of a store.
    
    	- The listener callback will be called immediately, and again whenever the state changed.
    • Returns an unsubscribe function. Call it to stop listening to the state.

      	```javascript
      	unsubscribe = store(function(state) {
      		alert(state.value);
      	});

      // stop listening unsubscribe();

      	```
  • store.someAction(arg1, arg2, ..., argN)

    	- Calls an action handler on the store, passing through any arguments.
    
    		```javascript
    		store = Hoverboard({
    			add: function(state, number) {
    				return (state || 0) + number;
    			}
    		});
    
    		store.add(5);
    		store.add(4);
    
    		result = store(); // returns 9
    		```

Hoverboard.compose

Hoverboard.compose takes a definition and creates a store, subscribing to any store members of the definition.

Hoverboard.compose can take static variables, objects or arrays.

// create two stores
var scoreStore = Hoverboard({
    add: function (state, score) {
        return state + score;
    }
}, 0);
var healthStore = Hoverboard({
    hit: function (state, amount) {
        return state - amount;
    }
}, 100);

// compose the two stores into a single store
var gameStore = Hoverboard.compose({
    score: scoreStore,

    // create an anonymous store to nest objects
    character: Hoverboard.compose({
        health: healthStore
    })
});

// stores and actions can be accessed with the same structure
gameStore.score.add(2);

gameStore.character.health.hit(1);

You can also pass zero or more translate functions after your compose definition, to automatically translate or map the state every time it gets updated.

These translate functions will receive a state argument, and must return the resulting state.

// create stores to contain the active and completed todos
var activeTodoStore = Hoverboard.compose(todoStore, function (todos) {
    return _.filter(todos, { completed: false });
});

var completedTodoStore = Hoverboard.compose(todoStore, function (todos) {
    return _.filter(todos, { completed: true });
});

FAQ

Q: Is this really Flux?

Yes. Flux requires that data flows in one direction, and Hoverboard enforces that.

When you call an action on a store, you can't get back a return value. The only way to get data out of a store is by registering a change listener. So this ensures that data flows following the Action -> Dispatcher -> Store -> View flow that is central to Flux.


Q: Does Hoverboard depend on React.js? Or can I use ____ instead?

You can use Hoverboard with any framework or library. It works really well with React, just because it's simple to pass an entire state object as props to a component, and have React figure out how to update the DOM efficiently.

That said, Hoverboard can still work with any other method of programming, but you might have to do more work to decide how to update your views when the state changes.

As other frameworks start adopting React's strategy of updating the DOM, Hoverboard will be a good fit to use with those frameworks as well.

Check out virtual-dom as an alternative to using React in your projects.


Q: Is Hoverboard universal? Can I use it on a node.js server?

Yes, it can work on the server. You can add listeners to stores, and render the page once you have everything you need from the stores. To be safe, you should probably unsubscribe from the store listeners once you've rendered the page, so you don't render it twice.

If you want to be able to "re-hydrate" your stores after rendering on the server, you can pass the initial state as the second parameter when creating your Hoverboard store.


Q: How does Hoverboard handle asynchronous loading of data from an API?

There are three ways to achieve this. One way is to load the API outside of the store, and call actions to pass in the loading state, data and/or error as it arrives:

var store = Hoverboard({
	loading: function(state, isLoading) {
		return { isLoading: isLoading };
	},
	data: function(state, data) {
		return { data: data };
	},
	error: function(state, error) {
		return { error: error };
	}
});

store.loading(true);

getDataFromAPI(params, function(error, data){
	store.loading(false);

	if (error) {
		return store.error(error);
	}

	store.data(data);
});

Another way is to call the API from within your store itself.

var store = Hoverboard({
	load: function (state, params) {
		getDataFromAPI(params, function (error, data) {
			store.done(error, data);
		});

		return { isLoading: true, error: null, data: null };
	},
	done: function(state, error, data) {
		return { isLoading: false, error: error, data: data };
	}
});

store.load(params);

Q: If Hoverboard stores only have a single getter, how can I have both getAll and getById?

You can use actions and state. If you have a list of items, and want to view a single item, then you might want to have an items property that contains the list, and an item property that contains the item you need. Something like this:

var initialState = { abc: 123, /* etc... */ };

var itemStore = Hoverboard({
	// update item whenever ID changes
	viewById: function (state, id) {
		return {
			item: state.items[id],
			items: state.items
		};
	},
	viewAll: function (state) {
		return {
			items: state.items
		}
	}
}, initialState);

// getAll
var items = itemStore().items;

// getById
itemStore.viewById(123);

var state = itemStore();

if (state.item) {
	// render single item
} else {
	// render list of items
}

Q: Hold on. There's no global dispatcher and there's no waitFor, so are you sure it's really Flux?

Yes. Ultimately Hoverboard acts as the dispatcher. Every action calls one specific action handler in one store, so the dispatching is simple. Like Facebook's Dispatcher, Hoverboard ensures that only one action is handled at a time, and won't allow action handlers to pass data back to the action caller.

waitFor is a mechanism that Facebook's Dispatcher provides to help multiple stores coordinate their response to a particular action. In Hoverboard, a store doesn't need to know about which actions were called, or if some asynchronous response triggered a change. Whenever one store changes, the other can update itself immediately.

How would you avoid using waitFor in Hoverboard? Let's compare using an example from the Facebook Flux Dispatcher tutorial:

Here's how the example from the link above would work with Hoverboard:

// Keeps track of which country is selected
var CountryStore = Hoverboard({
	update: function (state, selectedCountry) {
		return selectedCountry;
	}
});

// Keeps track of which city is selected
var CityStore = Hoverboard({
	update: function (state, selectedCity) {
		return selectedCity;
	}
});

// listen to the CountryStore
CountryStore(function (country) {
    // Select the default city for the new country
    if (country && CityStore() === undefined) {
		CityStore.update(getDefaultCityForCountry(country));
	}
});

// Keeps track of the base flight price of the selected city
var FlightPriceStore = Hoverboard({
	updatePrice: function (state, country, city) {
		return getFlightPriceStore(country, city);
	}
});

// called when either country or city change
function updateFlightPrice() {
	var country = CountryStore();
	var city = CityStore();

	FlightPriceStore.updatePrice(country, city);
}

// listen to changes from both the country and city stores
CountryStore(updateFlightPrice);
CityStore(updateFlightPrice);

// When a user changes the selected city, we call an action:
CityStore.update('paris');

// When the user selects a country, we call an action:
CountryStore.update('australia');

It's pretty much the same code, just written a different way. In both examples, the FlightPriceStore waits for the CountryStore and CityStore to change, and the flow of data moves through the same logic and processes.

Versioning

Hoverboard follows semver versioning. So you can be sure that the API won't change until the next major version.

Testing

Clone the GitHub repository, run npm install, and then run npm test to run the tests. Hoverboard has 100% test coverage.

Contributing

Feel free to fork this repository on GitHub, make some changes, and make a Pull Request.

You can also create an issue if you find a bug or want to request a feature.

Any comments and questions are very much welcome as well.

Author

Jesse Skinner @JesseSkinner

License

MIT

3.0.0-alpha.3

8 years ago

3.0.0-alpha.2

8 years ago

3.0.0-alpha

8 years ago

2.4.4

8 years ago

2.4.3

8 years ago

2.4.2

8 years ago

2.4.1

8 years ago

2.4.0

8 years ago

2.3.0

8 years ago

2.2.1

8 years ago

2.2.0

8 years ago

2.1.1

8 years ago

2.0.2

9 years ago

2.0.1

9 years ago

2.0.0

9 years ago

1.5.0

9 years ago

1.4.3

9 years ago

1.4.2

9 years ago

1.4.0

9 years ago

1.3.5

9 years ago

1.3.4

9 years ago

1.3.3

9 years ago

1.3.2

9 years ago

1.3.1

9 years ago

1.3.0

9 years ago

1.2.6

9 years ago

1.2.4

9 years ago

1.2.0

9 years ago

1.1.2

9 years ago

1.1.0

9 years ago

1.0.1

9 years ago

1.0.0

9 years ago

0.6.0

9 years ago

0.5.0

9 years ago

0.4.0

9 years ago

0.3.0

9 years ago

0.2.0

9 years ago

0.1.3

9 years ago

0.1.1

9 years ago

0.0.1

9 years ago