UNPKG

bdn-pocket

Version:

pocket tools for managing redux and redux-saga

673 lines (515 loc) 16.8 kB
- [BDN-POCKET](#bdn-pocket) - [Deprecated](#deprecated) - [Why bdn-pocket ?](#why-bdn-pocket) - [Definitions](#definitions) - [Project size](#project-size) - [Principles](#principles) - [Concepts](#concepts) - [Action](#action) - [Signal](#signal) - [Message](#message) - [Messenger](#messenger) - [Usage](#usage) - [PropTypes](#proptypes) - [Available types](#available-types) - [Signal](#signal) - [Create](#create) - [Change prefix](#change-prefix) - [PropTypes](#proptypes) - [Dispatch](#dispatch) - [Watch (in saga)](#watch-in-saga) - [Messenger](#messenger) - [Create](#create) - [Combine with [redux] `reducer` (`makeReducer`)](#combine-with-redux-reducer-makereducer) - [Call message (from saga)](#call-message-from-saga) - [Path reducer (`makePathReducer`)](#path-reducer-makepathreducer) - [Selector & SliceSelector](#selector-sliceselector) # 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] `action`s ## Project size This library is not intended to be used for small projects. `Action` is powerfull and flexible tools that helps to create `action`s. `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 `action`s * `Signal` generates `signal`s ### Signal A `Signal` is purely an `Action`, it creates an `action`. ````javascript 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` ````javascript 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. ````javascript 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 ````javascript // 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 ````javascript // 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. ````javascript // 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. ````javascript // 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 ````javascript // 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) ````javascript // 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 `message`s associated with [redux] `reducer`s. Helper functions help you to combine a `messenger` in the global [redux] `reducer`. #### Create ````javascript /// 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`) ````javascript /// 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 ````javascript /// 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` ````javascript // 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. ````javascript // 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`. ````javascript /// 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. ````javascript 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 } } }) ```` [redux]: https://github.com/reactjs/redux [redux-saga]: https://github.com/redux-saga/redux-saga [reselect]: https://github.com/reactjs/reselect [b-eden]: http://beden.b-flower.com/ [stampit]: https://github.com/stampit-org/stamp/tree/master/packages/it [Principles]: #principles