jesseskinner / hover

A very lightweight data store with action reducers and state change listeners.
MIT License
98 stars 7 forks source link
flux flux-architecture javascript react reactjs redux


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

Return value

store = new Hover(actions[, initialState])

store object methods

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