bdn-pocket
v1.2.1
Published
pocket tools for managing redux and redux-saga
Downloads
8
Readme
- BDN-POCKET
- Deprecated
- Why bdn-pocket ?
- Definitions
- Project size
- Principles
- Concepts
- Usage
- PropTypes
- Signal
- Messenger
- Create
- [Combine with redux
reducer
(makeReducer
)](#combine-with-redux-reducer-makereducer) - Call message (from saga)
- Path reducer (
makePathReducer
)
- 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
generatesaction
sSignal
generatessignal
s
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 reduxstate
- a
container
will always call aSignal
(not aMessage
)
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 reduxcontainer
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 message
s associated with redux reducer
s.
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: '[email protected]',
}
// 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
}
}
})