storeon

Tiny (173 bytes) event-based immutable state manager for React and Preact

Github星跟踪图

Storeon

A tiny event-based Redux-like state manager for React, Preact,
Angular, and Svelte.

  • 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.
  • 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
)

Tools

Third-party tools:

Install

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')

Store

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.

Events

There are three built-in events:

  • @init will be fired in createStore. The best moment to set
    an initial state.
  • @dispatch will be fired on every new action (on store.dispatch() calls and @changed event).
    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 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')
  }
})

Components

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.

DevTools

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')
])

TypeScript

Storeon delivers TypeScript declaration which allows to declare type
of state and optionally declare types of events and parameter.

If Storeon store has to be full type safe the event types declaration
interface has to be delivered as second type to createStore function.

import createStore, { Module } 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: Module<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 = createStore<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, it is considering adding
allowSyntheticDefaultImports: true to tsconfig.json.

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 = 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.

主要指标

概览
名称与所有者storeon/storeon
主编程语言JavaScript
编程语言JavaScript (语言数: 3)
平台
许可证MIT License
所有者活动
创建于2019-03-16 04:52:02
推送于2024-12-10 17:42:24
最后一次提交
发布数43
最新版本名称3.1.5 (发布于 2022-04-05 17:22:05)
第一版名称0.1.0 (发布于 2019-03-16 04:36:09)
用户参与
星数2k
关注者数19
派生数68
提交数370
已启用问题?
问题数69
打开的问题数11
拉请求数83
打开的拉请求数4
关闭的拉请求数13
项目设置
已启用Wiki?
已存档?
是复刻?
已锁定?
是镜像?
是私有?