Storeon
A tiny event-based Redux-like state manager for React, Preact,
Angular, Vue and Svelte.
- Small. 167 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.
- Modular. API created to move business logic away from React components.
Read more about Storeon features in our article.
import { createStoreon } from 'storeon'
let count = store => {
store.on('@init', () => ({ count: 0 }))
store.on('inc', ({ count }) => ({ count: count + 1 }))
}
export const store = createStoreon([count])
import { useStoreon } from 'storeon/react'
export const Counter = () => {
const { dispatch, count } = useStoreon('count')
return <button onClick={() => dispatch('inc')}>{count}</button>
}
import { StoreContext } from 'storeon/react'
render(
<StoreContext.Provider value={store}>
<Counter />
</StoreContext.Provider>,
document.body
)
Tools
Third-party tools:
Install
npm install storeon
If you need to support IE, you need to compile node_modules
with Babel and
add Object.assign
polyfill to your bundle. You should have this polyfill
already if you are using React.
import assign from 'object-assign'
Object.assign = assign
Store
The store should be created with the createStoreon()
function. It accepts a list
of functions.
Each function should accept a store
as the only argument and bind their event listeners using store.on()
.
import { createStoreon } from 'storeon'
import { projects } from './projects'
import { users } from './users'
export const store = createStoreon([projects, users])
export function projects (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.
Events
There are three built-in events:
@init
will be fired in createStoreon
. Bind to this event to set the initial state.@dispatch
will be fired on every new action (on store.dispatch()
calls
and @changed
events). It receives an array with the event name
and the event’s data. Can be useful for debugging.@changed
will be fired when any event changes the state.
It receives object with state changes.
To add an event listener, call store.on()
with the event name and a callback function.
store.on('@dispatch', (state, [event, data]) => {
console.log(`Storeon: ${ event } with `, data)
})
store.on()
will return a cleanup function. Calling 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.
store.on('@init', () => ({ users: { } }))
An event listener accepts the current state as the first argument
and optional event object as the second.
So event listeners can be reducers as well. As in Redux’s reducers,
your 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')
}
})
Components
For functional components, the useStoreon
hook will be the best option:
import { useStoreon } from 'storeon/react'
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 the connectStoreon()
decorator.
import { connectStoreon } from 'storeon/react'
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 connectStoreon('users', 'anotherStateKey', Users)
useStoreon
hook and connectStoreon()
accept the list of state keys to pass
into props
. It will re-render only if this keys will be changed.
DevTools
Storeon supports debugging with Redux DevTools Extension.
import { storeonDevtools } from 'storeon/devtools';
const store = createStoreon([
…
process.env.NODE_ENV !== 'production' && storeonDevtools
])
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 the built-in logger.
It could be useful for simple cases or to investigate issues in error trackers.
import { storeonLogger } from 'storeon/devtools';
const store = createStoreon([
…
process.env.NODE_ENV !== 'production' && storeonLogger
])
TypeScript
Storeon delivers TypeScript declarations which allows to declare type
of state and optionally declare types of events and parameter.
If a Storeon store has to be fully type safe the event types declaration
interface has to be delivered as second type to createStore
function.
import { createStoreon, StoreonModule } from 'storeon'
import { useStoreon } from 'storeon/react'
interface State {
counter: number
}
interface Events {
'inc': undefined
'set': number
}
const counterModule: StoreonModule<State, Events> = store => {
store.on('@init', () => ({ counter: 0}))
store.on('inc', state => ({ counter: state.counter + 1}))
store.on('set', (state, event) => ({ counter: event}))
}
const store = createStoreon<State, Events>([counterModule])
const Counter = () => {
const { dispatch, count } = useStoreon<State, Events>('count')
dispatch('set', 100)
dispatch('set', "100")
…
}
store.dispatch('set', 100)
store.dispatch('inc')
store.dispatch('inc', 100)
store.dispatch('set', "100")
store.dispatch('dec')
In order to work properly for imports, consider adding
allowSyntheticDefaultImports: true
to tsconfig.json
.
Server-Side Rendering
In order to preload data for server-side rendering, Storeon provides the
customContext
function to create your own useStoreon
hooks that
depend on your custom context.
import { createContext, render } from 'react'
import { createStoreon, StoreonModule } from 'storeon'
import { customContext } from 'storeon/react'
const store = …
const CustomContext = createContext(store)
export const useStoreon = customContext(CustomContext)
render(
<CustomContext.Provider value={store}>
<Counter />
</CustomContext.Provider>,
document.body
)
import { useStoreon } from '../store'
const Counter = () => {
const { dispatch, count } = useStoreon('count')
dispatch('set', 100)
…
}
Testing
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 = createStoreon([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 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.