A tiny event-based Redux-like state manager for React, Preact and Angular.
- Small. 173 bytes (minified and gzipped). No dependencies. It uses Size Limit to control size.
- Fast. It tracks what parts of state were changed and re-renders only components based on the changes.
- Hooks. The same Redux reducers. With hooks for React and Preact.
- Modular. API created to move business logic away from React components.
Read more about Storeon features in our article.
import createStore from 'storeon'
// Initial state, reducers and business logic are packed in independent modules
let increment = store => {
// Initial state
store.on('@init', () => ({ count: 0 }))
// Reducers returns only changed part of the state
store.on('inc', ({ count }) => ({ count: count + 1 }))
}
export const store = createStore([increment])
import useStoreon from 'storeon/react' // or storeon/preact
export default const Counter = () => {
// Counter will be re-render only on `state.count` changes
const { dispatch, count } = useStoreon('count')
return <button onClick={() => dispatch('inc')}>{count}</button>
}
import StoreContext from 'storeon/react/context'
render(
<StoreContext.Provider value={store}>
<Counter />
</StoreContext.Provider>,
document.body
)
@storeon/localstorage
saves and restores state tolocalStorage
.@storeon/crosstab
synchronizes events between browser tabs.
npm install storeon
If you need to support IE, add Object.assign
polyfill to your bundle.
You should have this polyfill already if you are using React.
Object.assign = require('object-assign')
The store should be created with createStore()
function. It accepts a list
of the modules.
Each module is just a function, which will accept a store
and bind their event listeners.
// store/index.js
import createStore from 'storeon'
import projects from './projects'
import users from './users'
export const store = createStore([projects, users])
// store/projects.js
export default store => {
store.on('@init', () => ({ projects: [] }))
store.on('projects/add', ({ projects }, project) => {
return { projects: projects.concat([project]) }
})
}
The store has 3 methods:
store.get()
will return current state. The state is always an object.store.on(event, callback)
will add an event listener.store.dispatch(event, data)
will emit an event with optional data.
There are three built-in events:
@init
will be fired increateStore
. The best moment to set an initial state.@dispatch
will be fired on everystore.dispatch()
call. It receives an array with the event name and the event’s data. Can be useful for debugging.@changed
will be fired every when event listeners changed the state. It receives object with state changes.
To add an event listener, call store.on()
with event name and callback.
store.on('@dispatch', (state, [event, data]) => {
console.log(`Storeon: ${ event } with `, data)
})
store.on()
will return cleanup function. This function will remove
the event listener.
const unbind = store.on('@changed', …)
unbind()
You can dispatch any other events. Just do not start event names with @
.
If the event listener returns an object, this object will update the state. You do not need to return the whole state, return an object with changed keys.
// users: {} will be added to state on initialization
store.on('@init', () => ({ users: { } }))
Event listener accepts the current state as a first argument and optional event object as a second.
So event listeners can be a reducer as well. As in Redux’s reducers, you should change immutable.
store.on('users/save', ({ users }, user) => {
return {
users: { ...users, [user.id]: user }
}
})
store.dispatch('users/save', { id: 1, name: 'Ivan' })
You can dispatch other events in event listeners. It can be useful for async operations.
store.on('users/add', async (state, user) => {
try {
await api.addUser(user)
store.dispatch('users/save', user)
} catch (e) {
store.dispatch('errors/server-error')
}
})
For functional components, useStoreon
hook will be the best option:
import useStoreon from 'storeon/react' // Use 'storeon/preact' for Preact
const Users = () => {
const { dispatch, users, projects } = useStoreon('users', 'projects')
const onAdd = useCallback(user => {
dispatch('users/add', user)
})
return <div>
{users.map(user => <User key={user.id} user={user} projects={projects} />)}
<NewUser onAdd={onAdd} />
</div>
}
For class components, you can use connect()
decorator.
import connect from 'storeon/react/connect' // Use 'storeon/preact/connect' for Preact
class Users extends React.Component {
onAdd = () => {
this.props.dispatch('users/add', user)
}
render () {
return <div>
{this.props.users.map(user => <User key={user.id} user={user} />)}
<NewUser onAdd={this.onAdd} />
</div>
}
}
export default connect('users', 'anotherStateKey', Users)
useStoreon
hook and connect()
accept the list of state keys to pass
into props
. It will re-render only if this keys will be changed.
Storeon supports debugging with Redux DevTools Extension.
const store = createStore([
…
process.env.NODE_ENV !== 'production' && require('storeon/devtools')
])
DevTools will also warn you about typo in event name. It will throw an error if you are dispatching event, but nobody subscribed to it.
Or if you want to print events to console
you can use built-in logger.
It could be useful for simple cases or to investigate issue in error trackers.
const store = createStore([
…
process.env.NODE_ENV !== 'production' && require('storeon/devtools/logger')
])
Tests for store can be written in this way:
it('creates users', () => {
let addUserResolve
jest.spyOn(api, 'addUser').mockImplementation(() => new Promise(resolve => {
addUserResolve = resolve
}))
let store = createStore([usersModule])
store.dispatch('users/add', { name: 'User' })
expect(api.addUser).toHaveBeenCalledWith({ name: 'User' })
expect(store.get().users).toEqual([])
addUserResolve()
expect(store.get().users).toEqual([{ name: 'User' }])
})
We recommend to keep business logic away from the components. In this case, UI kit (special page with all your components in all states) will be the best way to test components.
For instance, with UIBook you can mock store and show notification
on any dispatch
call.