Skip to content

musicq/store

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Store

Store is a state manager library which is built on top of RxJS. It's inspired by redux-observable, but doesn't depend on redux.

Philosophy

        dispatch                   monitor
view ---------------> action <----------------> side effects (e.g. async tasks)
 ∧                      |
 |                      |
 |                      |
 | subscribe            |
 |                      |
 |                      |
 |                      ∨
store <-------------- reducer
           update

Installation

Using npm

npm install @musicq/store

or using yarn

yarn add @musciq/store

Usage

First of all, we need to create a global store instance using createStore function.

Create a store.js file and append the below code.

import { createStore } from '@musicq/store'

export const store = createStore()

It will create a store instance. The store instance contains 2 objects, one is state$, which is the source of our state, the other is dispatch function, it is used to dispatch an action to update our state.

Next we need to create a reducer to reduce our state.

Create a file todo.js

export function todos(state = [], action) {
  switch (action.type) {
    case 'CREATE TODO':
      return [...state, action.payload]
    case 'DELETE TODO':
      return state.filter((t) => t.id !== action.payload)
    default:
      return state
  }
}

Then, import todo reducer into store.js file

import { todos } from './todo'

Pass todo reducer as a parameter of createStore

export const store = createStore(todos)

Everything is ready, let's play with it!

Create a app.js file.

import { store } from 'store'

store.state$.subscribe((state) => console.log(state))

store.dispatch({
  type: 'CREATE TODO',
  payload: { id: 1, content: 'Learn how to use @musicq/store' },
})

setTimeout(() => {
  store.dispatch({ type: 'DELETE TODO', payload: 1 })
}, 3000)

That looks great! However, how could we dispatch an async action? That's super easy in store.

Let's say we need to tell the server the new todo data via HTTP request, then update the store once the server responses.

To achieve that goal, let's create an effects.js file to manage our effect functions.

import { createEffect } from '@musicq/store'
import { switchMap } from 'rxjs/operators'
import { from } from 'rxjs'

export const effect = createEffect((action$, state$, dispatch) =>
  action$.pipe(
    filter((action) => action.type === 'SEND NEW TODO TO SERVER'),
    switchMap((action) =>
      // mock communication with server
      from(
        new Promise((resolve) => {
          setTimeout(() => resolve(action.payload), 1000)
        })
      )
    ),
    // Dispatch an action
    // This equals to `dispatch({type: 'CREATE TODO', payload: res})`
    map((res) => ({ type: 'CREATE TODO', payload: res }))
  )
)

Now, let's import our effects into store.js file and register it to the store to make it work.

import { effect } from './effects.js'

store.registerEffects([effect /* you can have multiple effects here */])

Then let change the dispatch action name in our app.js file

store.dispatch({
  type: 'SEND NEW TODO TO SERVER',
  payload: { id: 1, content: 'Learn how to use @musicq/store' },
})

Now when we run the app.js, it won't update the store immediately, instead, there will be a 1s delay, and then update the store.

So the flow will be

dispatch create action --- 1s later update the store --------- 3s later delete the todo from the store ---->

Types

Action

interface Action {
  type: string
  payload?: any
}

Reducer

type Reducer<T> = (state: T, action: Action) => T

EffectFn

export type EffectFn<T> = (
  action$: Subject<Action>,
  state$: Observable<T> & { value: T },
  dispatch: DispatchFn
) => Observable<Action | any>

API

createStore

To create a store instance

type createStore = <T>(reducers?: Reducer<T>) => {
  state$: Observable<T> & { value: T }
  dispatch: (action: Action) => void
  registerEffects: (effects: EffectFn<T>[]) => void
}

createEffect

type createEffect = <T>(
  fn: EffectFn<T>
) => (
  action$: Subject<Action>,
  state$: Observable<T> & { value: T },
  dispatch: DispatchFn
) => Observable<any>

combineReducers

Combine multiple reducers

type combineReducers = (reducers: {
  [key: string]: Reducer<any>
}) => Reducer<any>

For example, if you have a todo reducer and a user reducer. You can combine them up and pass it to the createStore.

function todos(state = [], action) {
  switch (action.type) {
    case 'CREATE TODO':
      return [...state, action.payload]
    default:
      return state
  }
}

function users(state = [], action) {
  switch (action.type) {
    case 'CREATE USER':
      return [...state, action.payload]
    default:
      return state
  }
}

const reducers = combineReducers({
  todos,
  users,
})

const store = createStore(reducers)