bdn-pocket

1.2.1 • Public • Published

BDN-POCKET

easily manage state management and state selection with redux and redux-saga

Deprecated

see: https://www.npmjs.com/package/@b-flower/bdn-pocket

Why bdn-pocket ?

bdn-pocket is a set of tools that helps to manage state and allow link between redux and redux-saga more easily.

It brings some new concepts like Signal, Message and Messenger that helps to separate ActionCreator in different categories with separation of responsabilities.

It allows Selector with arguments and a clean memoization (inspired by reselect and using it)

It enforces readibility and runtime validation with integration of propTypes on Action and Selector.

bdn-pocket has been built by b-eden development team. This project is an extract of differents concepts and development already existing in b-eden project and gathered now in this projet with some enhancements.

bdn-pocket uses stampit which brings composition and configuration with ease.

b-eden team plans to replace existing b-eden code with this library.

You can use this library with small project.

Definitions

Action = actionCreator => it generates redux actions

Project size

This library is not intended to be used for small projects.

Action is powerfull and flexible tools that helps to create actions.

Selector extends reselect with some new features and easy composition.

This library has been built for large projects using redux and redux-saga.

Read Principles for explanation.

Principles

At b-eden team we use redux with redux-saga for more than 2 years and that lead to some principles.

  • No side effect in a container (and component of course)
  • A container should not dispatch an action that change the state
  • All side effects should be done in redux-saga

Those principles help b-eden dev team to build a robust, maintenable, readable with comprehensive architecture app.

With separation of concern of Action between Signal and Message

Concepts

Action

Action is the same concept as redux one.

But in b-eden, Action are never used in favor of 2 new concepts (Signal, Messenger).

In bdn-pocket Action is an action creator.

  • Action generates actions
  • Signal generates signals

Signal

A Signal is purely an Action, it creates an action.

import { Signal, Action } from 'bdn-pocket'
console.log(Signal === Action) // => true

A signal must follow these principles:

  • a (dispatched) signal will never be used to change the redux state
  • a container will always call a Signal (not a Message)

Message

A message is an action called from a Messenger. It is associated to a reducer.

In bdn-pocket message is just a definition. Object Message does not exists as it is an Action in a Messenger.

Messenger

A Messenger is a tool that links a message defintion (Action) with a reducer (state).

A message is an action that will be associated with a reducer and will produce an new state.

A message must follows this principle:

  • never use a message in a redux container

Usage

PropTypes

Signal and Selector are composed of PropTypes (thanks to stampit).

Thus you can enforce props (as React propTypes) you receive and ensure you send good ones.

And it offers readibility and some documentation for the same price.

Available types

  • number
  • string
  • object
  • func
  • array
  • mixed
import { Signal, Types } from 'bdn-pocket'
const {
  number,
  string,
  object,
  func,
  array,
  mixed,
} = Types
 
const sig = Signal
  .propTypes({
    a: string, // required string
    b: string({ required: false }) // prop `b` not required but if present should not be null or undefined
    c: string({ required: false, allowNull: true }) // prop `c` not required and can be null
  })
  .def('my signal')
 
sig({ a: 'a', b: 'b', c: 'c' }) // => not throw
sig({ a: 'a', c: null}) // => not throw
sig({ b: 'b'}) // => throw an error
sig({ a: 'a', b: 10, c: null}) // => throw an error
sig({ a: 'a', b: null, c: null}) // => throw an error
 

You can use short notation to define prop types with required string.

import { Signal, Types } from 'bdn-pocket'
const {
  string,
} = Types
 
const sig = Signal
  .propTypes({
    a: string, // required string
    b: string, // required string
  })
  .def('my sig')
 
// same as
const sig = Signal
  .propTypes('a', 'b')
  .def('my sig')
 

Signal

A Signal is an action creator that creates a signal.

In the concept of bdn-pocket a dispatched signal should not result as a redux state change.

It's goal is to be watched by saga.

A redux container must dispatch a signal.

Create

// in /user/signal.js
import { Signal } from 'bdn-pocket'
 
const triggerLoadUser = Signal.def('trigger load user')
 
console.log(triggerLoadUser({ userId: 'me' } ))
// => { type: 'my-app/TRIGGER_LOAD_USER', payload: { userId: 'me' }}

Change prefix

// in /user/signal.js
import { Signal } from 'bdn-pocket'
 
const triggerLoadUser = Signal.prefix('my-plugin').def('trigger load user')
 
console.log(triggerLoadUser({ userId: 'me' } ))
// => { type: 'my-plugin/TRIGGER_LOAD_USER', payload: { userId: 'me' }}

You can use your own Signal definition inside a plugin.

// in /lib/my_signal.js
import { Signal } from 'bdn-pocket'
 
export default Signal.prefix('my-plugin')
 
// in /user/signal.js
import PluginSignal from '/lib/my-signal'
const triggerLoadUser = PluginSignal.def('trigger load user')
 
console.log(triggerLoadUser({ userId: 'me' } ))
// => { type: 'my-plugin/TRIGGER_LOAD_USER', payload: { userId: 'me' }}

PropTypes

You can enforce prop types of signal to ensure userId is present and has good type.

// in /user/signal.js
import { Signal, Types } from 'bdn-pocket'
const { string } = Types
 
const triggerLoadUser = Signal
  .propTypes({
    userId: string
  })
  .def('trigger load user')
 
console.log(triggerLoadUser({ userId: 'me' } ))
// => { type: 'my-plugin/TRIGGER_LOAD_USER', payload: { userId: 'me' }}
 
// call without userId throw an error
triggerLoadUser({NOUSERID: ''}) // => throw an error

Dispatch

// in /module/user/signal.js
import { Signal } from 'bdn-pocket'
 
export const triggerLoadUser = Signal.def('trigger load user')
 
// in /module/user/container
import { connect } from 'react-redux'
 
import * as userSig from '../signals' // <- signals are here
import MyComp from '../component/my_comp'
 
export default connect(
  null,
  function dispatchToProps(dispatch) {
    return {
      loadUser(userId) {
        dispatch(userSig.triggerLoadUser({ userId }) // <- signal accept only one arg
      }
    }
  }
)(MyComp)
 

Watch (in saga)

// in /module/user/signal.js
import { Signal } from 'bdn-pocket'
 
export const triggerLoadUser = Signal.def('trigger load user')
 
// in /module/user/sagas
import { take, call } from 'redux-saga/effects'
import * as userSig from '../signals' // <- signals are here
 
export function* watchLoadUser() {
  while(true) {
    const { payload } = yield take(userSig.triggerLoadUser.CONST)
    yield call(loadUser, payload)
  }
}
 
function* loadUser({ userId }) {
  // do some side effect here
}
 

Messenger

A Messenger is a list of messages associated with redux reducers.

Helper functions help you to combine a messenger in the global redux reducer.

Create

/// in /module/user/messages.js
 
import {
  Messenger,
  Action,
  Types,
  } from 'bdn-pocket'
import R from 'ramda' // <- yes we use Ramda a lot
 
const { string } = Types
const state = {
  users: {
    'user1': {
      id: 'a',
      name: 'name',
      email: 'email',
    }
  }
}
export const user = Messenger
  .add({
    key: 'add',
    action: Action
      .propTypes('id', 'name', 'email')
      .def('add user'),
    reducer(state, { payload: { id, name, email } }) {
      return R.assoc(
        id,
        { id, name, email },
        state
      )
    }
  })
  .add({
    key: 'del',
    action: Action
      .propTypes('id')
      .def('del user'),
    reducer(state, { payload: { id } }) {
      return R.dissoc(
        id,
        state
      )
    }
  })
  .add({
    key: 'update',
    action: Action
      .propTypes({
        id: string,
        name: string(required: false),
        name: string(required: false),
      })
      .def('update user'),
    reducer(state, { payload: data }) {
      return R.mergeWith( // <- yes, it is a special ramda trick
        R.merge,
        state,
        { [data.id]:  userData }
      )
    }
  })
  .create({ name: 'user entities' }) // <- DO NOT FORGET TO CREATE YOUR MESSENGER INSTANCE
 
 

Combine with redux reducer (makeReducer)

/// in /module/user/messages.js
 
import {
  Messenger,
  Action,
  makeReducer, // <- here we added makeReducer
} from 'bdn-pocket'
import R from 'ramda' // <- yes we use Ramda a lot
// ... same code as before
 
export default makeReducer(user) // <- now you can combine this reducer with global redux reducer

If you define more than one messenger in a messenger file, you can use redux combineReducer helper to export a default reducer from your file

/// in /module/user/messages.js
 
import {
  Messenger,
  Action,
  makeReducer, // <- here we added makeReducer
} from 'bdn-pocket'
import R from 'ramda' // <- yes we use Ramda a lot
import { combineRecuers } from 'redux'
 
export const user = Messenger
  .add({
    ...
  })
  .create({ name: 'user' })
 
export const account = Messenger
  .add({
    ...
  })
  .create({ name: 'account' })
 
export default combineReducer({
  user: makeReducer(user),
  account: makeReducer(account)
}) // <- now you can combine this reducer with global redux reducer
 

Call message (from saga)

A message has to be dispatch in order to call associated reducer.

To create a message you have to use the message create accessible with key on messenger

 
// in /module/user/sagas
import { take, call, put } from 'redux-saga/effects'
import * as userSig from '../signals'
import * as userMsg from '../messages'
 
export function* watchLoadUser() {
  while(true) {
    const { payload } = yield take(userSig.triggerLoadUser.CONST)
    yield call(loadUser, payload)
  }
}
 
function* loadUser({ userId }) {
  // do some side effect here ...
  // here we assume we receive a user data from our server
  const userData = {
    id: 'me',
    name: 'Arnaud',
    email: 'amelon@b-flower.com',
  }
 
  //                add is the key of Message in messenger
  yield put(userMsg.add(userData)) // <-- create message -> dispatch (put) -> call reducer -> new state
}
 
// other exemple
function* delUser({ userId }) {
  // do some server stuff ...
  // now delete user in state
 
  //                del is the key of Message creator in messenger
  yield put(userMsg.del({ id: userId }))
}
 

Path reducer (makePathReducer)

Sometimes (often) you want to use payload property as key of a substate.

In our previous exemple, we use payload.id to put a specific user data under this sub state.

// example of our users state
const state = {
  users: {
    me: { // id is used as key in our users sub state
      id: 'me',
      name: 'Arnaud',
      //...
    }
  }
}

To facilitate this common pattern, we use makePathReducer.

/// in /module/user/messages.js
 
import {
  Messenger,
  Action,
  Types,
  makePathReducer,
  } from 'bdn-pocket'
import R from 'ramda' // <- yes we use Ramda a lot
 
const { string } = Types
const { DELETE_KEY } = makePathReducer
 
// here state manipulation is easier
// state in reducer is directly `users.id` (in first exemple it was `users`)
export const user = Messenger
  .add({
    key: 'add',
    action: Action
      .propTypes('id', 'name', 'email')
      .def('add user'),
    reducer(state, { payload }) {
      return payload
    }
  })
  .add({
    key: 'del',
    action: Action
      .propTypes('id')
      .def('del user'),
    reducer(state, { payload: { id } }) {
      return DELETE_KEY // special trick => will remove key from state
    }
  })
  .add({
    key: 'update',
    action: Action
      .propTypes({
        id: string,
        name: string(required: false),
        name: string(required: false),
      })
      .def('update user'),
    reducer(state, { payload: data }) {
      return R.merge(state, data)
 
    }
  })
  .create({ name: 'user entities' }) // <- DO NOT FORGET TO CREATE YOUR MESSENGER INSTANCE
 
export default makePathReducer(
  user,
  (payload) => payload.id
  // or even simpler
  // ({ id }) => id
)
 

Selector & SliceSelector

reselect is a wonderfull library but it misses selector with arguments.

With Selector you can send props to your selector to make some filtering or choices.

You can ensure your props as Selector is composed of PropTypes.

Selector used reselect under the hood and implements it's own memoization to handle props.

You can compose a Selector with another Selector (see getArticle example)

A composed Selector (see userSel in getArticle) return a partial function that is memoized once.

It is usefull for computation selection.

Do not use Selector to get a slice of state. Use SliceSelector in this case.

As Selector memoizes the last reducer result, if you want to only get a portion of your state without any computation, it won't be performant to run memoization and props comparison check.

import { Selector, SliceSelector } from 'bdn-pocket'
const state = {
  "articles": {
    "123": {
      id: "123",
      author: "1",
      title: "My awesome blog post",
      comments: [ "324" ]
    }
  },
  "users": {
    "1": { "id": "1", "name": "Paul" },
    "2": { "id": "2", "name": "Nicole" }
  },
  "comments": {
    "324": { id: "324", "commenter": "2" }
  }
}
 
const getSlice = (name) => (state) => state[name]
 
const getUsers = getSlice('users')
 
const getUser = SliceSelector
  .selectors({
    users: getUsers
  })
  .propTypes('userId')
  .create({
    // first arg = list of sub states
    // second arg = list of props send
    reducer({ users },  { userId }) {
      return users[userId]
    }
  })
 
 
const getComments = state => state.comments
 
const getComment = SliceSelector
  .selectors({
    comments: getComments,
  })
  .propTypes('commentId')
  .create({
    reducer({ comments }, { commentId }) {
      return comments[commentId]
    }
  })
 
 
const getArticles = state => state.articles
 
const getArticle = Selector
  .selectors({
    userSel: getUser,
    commentSel: getComment,
    articles: getArticles,
  })
  .propTypes('articleId')
  .create({
    reducer({ userSel, commentSel, articles }, { articleId }) {
      // userSel & commentSel are partial functions that wait for theirs props ({userId} for userSel, { commentId } for commentSel )
      const article = articles[articleId]
      const comments = article.comments.map(
        commentId => {
          const comment = commentSel({ commentId })
          const user = userSel({ userId: comment.commenter })
          return {
            comment,
            commenter: user
          }
        }
      )
      return {
        article,
        comments
      }
    }
  })
 
 

Dependents (0)

Package Sidebar

Install

npm i bdn-pocket

Weekly Downloads

0

Version

1.2.1

License

MIT

Last publish

Collaborators

  • gabchang
  • amelon