Skip to content

Latest commit

 

History

History
377 lines (260 loc) · 9.38 KB

README.md

File metadata and controls

377 lines (260 loc) · 9.38 KB


A very lightweight data store
with action reducers
and state change listeners.


NPM version Downloads Build Status Coverage Status Dependency status Dev Dependency status

Installation

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

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

npm install hover

import Hover from 'hover'

Concept

Hover is a place to keep your state. You can pass in an initial state, but the only way to change the state afterwards is through action reducers you define.

The basic usage of Hover is:

// in store.js
import Hover from 'hover'

const actions = {
	increment: (state, amount) => state + amount
}

const initialState = 0

export default new Hover(actions, initialState)

// elsewhere
import store from './store'
const state = store.increment(2)

You can easily subscribe to state changes with Hover stores. You can pass a callback to the store. Your callback will be called immediately at first, and again whenever the state changes. Here's an example using vanilla DOM scripting to update the page:

function renderUserProfile (user) {
	if (user) {
		const root = document.getElementById('user-profile'),
			avatar = root.querySelector('.avatar'),
			name = root.querySelector('.name')

		// use the avatar url as an image source
		avatar.src = user.avatar

		// erase previous contents of name
		while (name.firstChild) {
			name.removeChild(name.firstChild)
		}

		// add name as a text node
		name.appendChild(document.createTextNode(user.name))
	}
}

userStore(renderUserProfile)

Here's an example rendering a React component:

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

userStore(renderUserProfile)

Usage

Here's how you might use Hover to keep track of clicks with a clickCounter.

const actions = {
	click: (state, text) => ({
		value: state.value + 1,
		log: state.log.concat(text)
	}),

	// go back to defaults
	reset: () => initialState
}

const initialState = 0

const clickCounter = new Hover(actions, initialState)

// listen to changes to the state
const unsubscribe = clickCounter(clickState =>
	document.write(JSON.stringify(clickState) + "<br>"
)

clickCounter.click('first')
clickCounter.click('second')

// reset back to zero
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 Hover can fit into a larger app, with React and a router, check out the Hover TodoMVC.

Documentation

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

Syntax

store = new Hover(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.

     // store is synchronous, actions are setters
     store = new Hover({
     	items: (state, items) => ({ items }),
     	error: (state, error) => ({ error })
     }, {})
    
     // load data asynchronously and call actions to change the state
     api.getItems((error, items) => {
     	if (error) {
     		return store.error(error)
     	}
    
     	store.items(items)
     })
    
     // listen to the state, and respond to it accordingly
     store(state => {
     	if (state.items) {
     		renderItems(state.items)
     	} else if (state.error) {
     		alert('Error loading items!')
     	}
     })

Return value

store = new Hover(actions[, initialState])

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.

       unsubscribe = store(state => console.log(state))
      
       // stop listening
       unsubscribe()
  • state = store.action(arg0, arg1, ..., argN)

    • Calls an action handler on the store, passing through any arguments.

       store = new Hover({
       	add: (state, number) => state + number
       }, 0)
      
       result = store() // returns 0
       result = store.add(5) // returns 5
       result = store.add(4) // returns 9
       result = store() // returns 9

Hover.compose

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

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

// create two stores
const scoreStore = new Hover({
    add: (state, score) => state + score
}, 0)
const healthStore = new Hover({
    hit: (state, amount) => state - amount
}, 100)

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

    // create an anonymous store to nest objects
    character: Hover.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
const activeTodoStore = Hover.compose(todoStore, todos =>
    todos.filter(todo => todo.completed === false)
)

const completedTodoStore = Hover.compose(todoStore, todos =>
    todos.filter(todo => todo.completed === true)
})

FAQ

Q: How does Hover 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:

const store = new Hover({
	loading: (state, isLoading) => ({ isLoading }),
	data: (state, data) => ({ data }),
	error: (state, error) => ({ error })
})

store.loading(true)

getDataFromAPI(params, (error, data) => {
	if (error) {
		return store.error(error)
	}

	store.data(data)
})

Another way is to make API calls from inside your actions.

const store = new Hover({
	load: (state, params) => {
		getDataFromAPI(params, (error, data) =>
			store.done(error, data)
		)

		return { isLoading: true, error: null, data: null }
	},
	done: (state, error, data) => (
		{ isLoading: false, error, data }
	)
})

store.load(params)

Q: If Hover stores only have a single getter, how can I have something like getById?

If you have access to a list of items in the state, you can write code to search through the list. You could even have a function like this as a property of the store, before you export it, eg.

import Hover from 'hover'

const initialState = [{ id: 1, name: 'one' }, /* etc... */ }

const itemStore = new Hover({
	add: (list, item) => list.concat(item)
}, initialState)

// add a helper function to the store
itemStore.getById = id =>
	list.filter(item => item.id === id).pop()

// getAll
const items = itemStore()

// look up a specific item
const item = itemStore.getById(5)

Versioning

Hover 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. Hover 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