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

    Install

    npm i bdn-pocket

    DownloadsWeekly Downloads

    1

    Version

    1.2.1

    License

    MIT

    Last publish

    Collaborators

    • gabchang
    • amelon