storeon
🌩 A tiny (185 bytes) event-based Redux-like state manager for React, Preact, Angular, Vue, and Svelte
Top Related Projects
A JS library for predictable global state management
Simple, scalable state management.
🐻 Bear necessities for state management in React
Business logic with ease ☄️
🌶 350b / 650b state container with component actions for Preact & React
Actor-based state management & orchestration for complex app logic.
Quick Overview
Storeon is a tiny (173 bytes) event-driven state manager for JavaScript applications. It follows the KISS principle and provides a minimalistic API for managing application state, making it suitable for both small and large projects.
Pros
- Extremely lightweight, with minimal impact on bundle size
- Simple and easy to learn API
- Works well with React, Preact, and vanilla JS
- Supports TypeScript out of the box
Cons
- Limited ecosystem compared to more established state management libraries
- May lack advanced features found in larger state management solutions
- Not as widely adopted, which could mean less community support
Code Examples
Creating a store:
import { createStoreon } from 'storeon'
const counter = store => {
store.on('@init', () => ({ count: 0 }))
store.on('increment', ({ count }) => ({ count: count + 1 }))
}
const store = createStoreon([counter])
Using the store in a React component:
import { useStoreon } from 'storeon/react'
const Counter = () => {
const { dispatch, count } = useStoreon('count')
return <button onClick={() => dispatch('increment')}>{count}</button>
}
Subscribing to store changes:
const unbind = store.on('@changed', (state, changed) => {
console.log('State changed', changed)
})
// Later, to unsubscribe
unbind()
Getting Started
-
Install Storeon:
npm install storeon
-
Create a store:
import { createStoreon } from 'storeon' const counter = store => { store.on('@init', () => ({ count: 0 })) store.on('increment', ({ count }) => ({ count: count + 1 })) } export const store = createStoreon([counter])
-
Use the store in your app:
import { StoreContext } from 'storeon/react' import { store } from './store' ReactDOM.render( <StoreContext.Provider value={store}> <App /> </StoreContext.Provider>, document.getElementById('root') )
Competitor Comparisons
A JS library for predictable global state management
Pros of Redux
- Extensive ecosystem with middleware, dev tools, and community support
- Well-established patterns for complex state management
- Robust documentation and learning resources
Cons of Redux
- Steeper learning curve and more boilerplate code
- Larger bundle size, potentially impacting performance
- Can be overkill for simpler applications
Code Comparison
Redux:
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
default:
return state;
}
};
Storeon:
const counter = store => {
store.on('@init', () => ({ count: 0 }));
store.on('increment', ({ count }) => ({ count: count + 1 }));
};
Redux requires more setup code and uses a switch statement for actions, while Storeon has a more concise syntax with event-based state updates. Redux's approach allows for more complex state transformations, but Storeon's simplicity can lead to cleaner code for smaller applications.
Both libraries aim to provide predictable state management, but Redux offers more features and flexibility at the cost of complexity, while Storeon focuses on minimalism and performance. The choice between them depends on the project's scale and requirements.
Simple, scalable state management.
Pros of MobX
- More mature and widely adopted, with a larger ecosystem and community support
- Offers automatic tracking of observables, reducing boilerplate code
- Provides powerful debugging tools and React integration out of the box
Cons of MobX
- Larger bundle size and potentially higher learning curve
- Can lead to less explicit data flow, making it harder to reason about state changes
- More complex API with multiple concepts to understand (e.g., observables, actions, computed values)
Code Comparison
MobX:
import { makeObservable, observable, action } from "mobx";
class Store {
count = 0;
constructor() {
makeObservable(this, {
count: observable,
increment: action
});
}
increment() {
this.count++;
}
}
Storeon:
import { createStore } from "storeon";
const counter = store => {
store.on("@init", () => ({ count: 0 }));
store.on("increment", ({ count }) => ({ count: count + 1 }));
};
const store = createStore([counter]);
The code comparison demonstrates that Storeon has a simpler API with a more functional approach, while MobX uses a class-based structure with decorators for observables and actions. Storeon's event-based system is more explicit, whereas MobX relies on automatic tracking of observables.
🐻 Bear necessities for state management in React
Pros of Zustand
- More flexible API with support for multiple stores and partial updates
- Built-in middleware system for extending functionality
- Larger community and ecosystem with more third-party integrations
Cons of Zustand
- Slightly larger bundle size compared to Storeon
- More complex API may have a steeper learning curve for beginners
- Less emphasis on minimalism and simplicity
Code Comparison
Zustand:
import create from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
Storeon:
import { createStoreon } from 'storeon'
const store = createStoreon([
(store) => {
store.on('@init', () => ({ count: 0 }))
store.on('increment', ({ count }) => ({ count: count + 1 }))
}
])
Summary
Zustand offers more features and flexibility, making it suitable for larger applications with complex state management needs. Storeon, on the other hand, focuses on simplicity and minimal bundle size, which can be advantageous for smaller projects or when performance is a top priority. The choice between the two depends on the specific requirements of your project and personal preferences regarding API design and feature set.
Business logic with ease ☄️
Pros of Effector
- More powerful and flexible, offering advanced features like computed stores and effect handlers
- Better TypeScript support with stronger type inference
- Larger ecosystem with additional tools and libraries
Cons of Effector
- Steeper learning curve due to more complex API and concepts
- Larger bundle size, which may impact performance in smaller applications
- More boilerplate code required for basic state management tasks
Code Comparison
Storeon:
import { createStoreon } from 'storeon'
const counter = store => {
store.on('@init', () => ({ count: 0 }))
store.on('increment', ({ count }) => ({ count: count + 1 }))
}
const store = createStoreon([counter])
Effector:
import { createStore, createEvent } from 'effector'
const increment = createEvent()
const $counter = createStore(0)
.on(increment, state => state + 1)
$counter.watch(state => console.log('Counter:', state))
increment()
Both libraries provide simple state management solutions, but Effector offers more advanced features at the cost of increased complexity. Storeon focuses on minimalism and simplicity, while Effector provides a more comprehensive toolkit for larger applications with complex state management needs.
🌶 350b / 650b state container with component actions for Preact & React
Pros of Unistore
- Smaller bundle size (about 350 bytes)
- Simpler API with less boilerplate
- Supports both React and Preact out of the box
Cons of Unistore
- Less flexible than Storeon for complex state management
- Fewer built-in features for debugging and middleware
- Not as actively maintained (last update was in 2021)
Code Comparison
Unistore:
import createStore from 'unistore'
const store = createStore({ count: 0 })
const actions = store => ({
increment: ({ count }) => ({ count: count + 1 })
})
Storeon:
import { createStoreon } from 'storeon'
const counter = store => {
store.on('@init', () => ({ count: 0 }))
store.on('increment', ({ count }) => ({ count: count + 1 }))
}
const store = createStoreon([counter])
Both libraries aim to provide lightweight state management solutions, but Storeon offers a more modular approach with event-based architecture. Unistore is simpler and more straightforward for basic use cases, while Storeon provides more flexibility for complex applications. The choice between them depends on the specific needs of your project and your preferred coding style.
Actor-based state management & orchestration for complex app logic.
Pros of XState
- More powerful and feature-rich, supporting complex state machines and statecharts
- Excellent visualization tools for debugging and understanding state transitions
- Extensive documentation and community support
Cons of XState
- Steeper learning curve due to its complexity and advanced features
- Larger bundle size, which may impact performance in smaller applications
- Can be overkill for simple state management needs
Code Comparison
XState:
import { createMachine, interpret } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
});
Storeon:
import createStore from 'storeon'
const counter = store => {
store.on('@init', () => ({ count: 0 }))
store.on('inc', ({ count }) => ({ count: count + 1 }))
}
const store = createStore([counter])
XState offers a more declarative approach to defining state machines, while Storeon provides a simpler API for basic state management. XState is better suited for complex applications with intricate state logic, whereas Storeon excels in lightweight scenarios where minimal overhead is desired.
Convert
designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual CopilotREADME
Storeon
Deprecated in favor of Nano Stores.
A tiny event-based Redux-like state manager for React, Preact, Angular, Vue and Svelte.
- Small. 180 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'
// Initial state, reducers and business logic are packed in independent modules
let count = 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 = createStoreon([count])
import { useStoreon } from 'storeon/react' // or storeon/preact
export 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'
render(
<StoreContext.Provider value={store}>
<Counter />
</StoreContext.Provider>,
document.body
)
Tools
@storeon/router
tracks links and Back button click and allows you to open pages without reloading the whole page.@storeon/localstorage
saves and restores state tolocalStorage
orsessionStorage
.@storeon/crosstab
synchronizes events between browser tabs.@storeon/undo
allows undoing or redoing the latest event.@storeon/websocket
to sync actions through WebSocket.
Third-party tools:
majo44/storeon-async-router
is router with data prefetch, modules lazy load, navigation cancellation, and routes modification on the fly.mariosant/storeon-streams
is side effects management library.octav47/storeonize
is migrating tool from Redux to Storeon.
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()
.
// store/index.js
import { createStoreon } from 'storeon'
import { projects } from './projects'
import { users } from './users'
export const store = createStoreon([projects, users])
// store/projects.js
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 increateStoreon
. Bind to this event to set the initial state.@dispatch
will be fired on every new action (onstore.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.
// users: {} will be added to state on initialization
store.on('@init', () => ({ users: { } }))
An event listener accepts the current state as the first argument, optional event object as the second and optional store object as the third.
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' // 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 the connectStoreon()
decorator.
import { connectStoreon } from 'storeon/react' // Use 'storeon/preact' 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 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' // or storeon/preact
// State structure
interface State {
counter: number
}
// Events declaration: map of event names to type of event data
interface Events {
// `inc` event which do not goes with any data
'inc': undefined
// `set` event which goes with number as data
'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')
// Correct call
dispatch('set', 100)
// Compilation error: `set` event do not expect string data
dispatch('set', "100")
â¦
}
// Correct calls:
store.dispatch('set', 100)
store.dispatch('inc')
// Compilation errors:
store.dispatch('inc', 100) // `inc` doesnât have data
store.dispatch('set', "100") // `set` event do not expect string data
store.dispatch('dec') // Unknown event
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.
// store.jsx
import { createContext, render } from 'react' // or preact
import { createStoreon, StoreonModule } from 'storeon'
import { customContext } from 'storeon/react' // or storeon/preact
const store = â¦
const CustomContext = createContext(store)
// useStoreon will automatically recognize your storeon store and event types
export const useStoreon = customContext(CustomContext)
render(
<CustomContext.Provider value={store}>
<Counter />
</CustomContext.Provider>,
document.body
)
// children.jsx
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.
Top Related Projects
A JS library for predictable global state management
Simple, scalable state management.
🐻 Bear necessities for state management in React
Business logic with ease ☄️
🌶 350b / 650b state container with component actions for Preact & React
Actor-based state management & orchestration for complex app logic.
Convert
designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual Copilot