Giter VIP home page Giter VIP logo

storeon's Introduction

Storeon

Storeon logo by Anton Lovchikov

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
)
Sponsored by Evil Martians

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

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

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

storeon's People

Contributors

ai avatar andarist avatar antiflasher avatar beraliv avatar bhovhannes avatar dependabot[bot] avatar distolma avatar feildmaster avatar felixcatto avatar gosolivs avatar hadeeb avatar igorkamyshev avatar imekachi avatar irustm avatar jamesramm avatar johakr avatar lodin avatar majo44 avatar mariosant avatar octav47 avatar polemius avatar rayriffy avatar rdmrcv avatar revich2 avatar ruimarques avatar shoonia avatar sivakov512 avatar tdiam avatar ty3uk avatar wrinklej avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    šŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. šŸ“ŠšŸ“ˆšŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ā¤ļø Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.