A tiny event-based Redux-like state manager for React and Preact.
- Small. 175 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. Now with hooks for React/Preact.
- Modular. API created to move business logic away from React components.
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
)
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.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', ([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.
// count: 0 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 connect from 'storeon/react' // Use 'storeon/preact' for Preact
const Users = () => {
const { dispatch, users } = useStoreon('users')
const onAdd = useCallback(user => {
dispatch('users/add', user)
})
return <div>
{users.map(user => <User key={user.id} user={user} />)}
<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 User extends React {
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', Users)
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')
])
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
on online error trackers.
const store = createStore([
…
process.env.NODE_ENV !== 'production' && require('storeon/devtools/logger')
])