bdux
v18.0.5
Published
A Flux implementation
Downloads
54
Maintainers
Readme
Bdux
A Flux architecture implementation out of enjoyment of Bacon.js, Redux and React.
Want to achieve
- Reactive all the way from action to React component.
- Redux style time travel through middleware and reducer.
- Only activate reducer when there is a subscriber.
- Utilise stateless functional React component.
- Utilise React context provider and consumer.
Installation
To install as an npm package:
npm install --save bdux
Action
Action creator returns:
- A single action object.
- A Bacon stream of action objects.
- A falsy value to create no action.
Example of action creators:
import Bacon from 'baconjs'
import ActionTypes from './action-types'
export const add = () => ({
type: ActionTypes.ADD
})
export const complete = () => (
Bacon.once({
type: ActionTypes.COMPLETE
})
)
export const remove = (index) => {
if (index >= 0) {
return {
type: ActionTypes.REMOVE
}
}
}
Store
Store is created using createStore(name, getReducer, otherStores = {})
.
name
specifies a unique store name, which can be:- A string.
- A function
props => ({ name })
.
getReducer
returns a reducer asPluggable
which is an object contains the input and output of a stream.otherStores
is an object of dependent stores.
Reducer stream:
- Receives an input object
{ action, state, dispatch, bindToDispatch, ...dependencies }
. - Should always output the next state according purely on the input object.
- Should NOT have intermediate state. e.g.
scan
orskipDuplicates
. - Should NOT have side effect. e.g.
flatMap
orthrottle
.
Have intermediate states and side effects in action creators instead. So time travelling can be achieved, and there is a single point to monitor all actions which could cause state changes. Store can dispatch actions which will be queued to cause state changes in other stores.
Example of a store:
import R from 'ramda'
import Bacon from 'baconjs'
import ActionTypes from '../actions/action-types'
import StoreNames from '../stores/store-names'
import { createStore } from 'bdux'
const isAction = R.pathEq(
['action', 'type']
)
const whenCancel = R.when(
isAction(ActionTypes.CANCEL),
R.assocPath(['state', 'confirm'], false)
)
const whenConfirm = R.when(
isAction(ActionTypes.CONFIRM),
R.assocPath(['state', 'confirm'], true)
)
const getOutputStream = (reducerStream) => (
reducerStream
.map(whenCancel)
.map(whenConfirm)
.map(R.prop('state'))
)
export const getReducer = () => {
const reducerStream = new Bacon.Bus()
return {
input: reducerStream,
output: getOutputStream(reducerStream)
}
}
export default createStore(
StoreNames.DIALOG, getReducer
)
Dealing with a collection of data is a common and repetitive theme for store. Creating a separate store for the items in the collection can be a great tool for the scenario. Simply construct the store names dynamically from props
for individual items.
Example of constrcuting store names:
const getConfig = props => ({
name: `${StoreNames.PRODUCT}_${props.productId}`,
// mark the store instance as removable
// to be removed on component unmount.
isRemovable: true,
// default value will be null if not configured.
defaultValue: {
items: [],
},
})
export default createStore(
getConfig, getReducer
)
Component
Component can subscribe to dependent stores using hooks useBdux(props, stores = {}, callbacks = [], skipDuplicates)
or createUseBdux(stores = {}, callbacks = [], skipDuplicates)(props)
.
stores
is an object of dependent stores.callbacks
is any array of functions to be triggered after subscribing to stores.skipDuplicates
is a function to map store properties. The default behaviour ismap(property => property.skipDuplicates())
.
The hooks return an object of:
state
is an object of the current values of stores.dispatch
is a function to dispatch the return value of an action creator to stores.bindToDispatch
binds a single action creator or an object of action creators to dispatch actions to stores.
Example of a component:
import R from 'ramda'
import React, { useMemo, useCallback } from 'react'
import * as CountDownAction from '../actions/countdown-action'
import CountDownStore from '../stores/countdown-store'
import { createUseBdux } from 'bdux'
const useBdux = createUseBdux({
countdown: CountDownStore
}, [
// start counting down.
CountDownAction.countdown
])
const CountDown = (props) => {
const { state, dispatch, bindToDispatch } = useBdux(props)
const handleClick = useMemo(() => (
bindToDispatch(CountDownAction.click)
), [bindToDispatch])
const handleDoubleClick = useCallback(() => {
dispatch(CountDownAction.doubleClick())
}, [dispatch])
return R.is(Number, state.countdown) && (
<button
onClick={ handleClick }
onDoubleClick={ handleDoubleClick }
>
{ state.countdown }
</button>
)
}
export default React.memo(CountDown)
Wrap the entire app in a bdux context provider optionally to avoid of using global dispatcher and stores, which is also useful for server side rendering to isolate requests.
import React from 'react'
import { createRoot } from 'react-dom/client';
import { BduxContext, createDispatcher } from 'bdux'
import App from './components/app'
const bduxContext = {
dispatcher: createDispatcher(),
stores: new WeakMap()
}
const renderApp = () => (
<BduxContext.Provider value={bduxContext}>
<App />
</BduxContext.Provider>
)
createRoot(document.getElementById('app'));
.render(renderApp())
Middleware
Middleware exports getPreReduce
, getPostReduce
and useHook
optionally.
getPreReduce
returns aPluggable
stream to be applied before all reducers.getPostReduce
returns aPluggable
stream to be applied after reducers.useHook
is triggered in all components which includeuseBdux
.
Example of a middleware:
import Bacon from 'baconjs'
const logPreReduce = ({ action }) => {
console.log('before reducer')
}
const logPostReduce = ({ nextState }) => {
console.log('after reducer')
}
export const getPreReduce = (/*{ name, dispatch, bindToDispatch }*/) => {
const preStream = new Bacon.Bus()
return {
input: preStream,
output: preStream
.doAction(logPreReduce)
}
}
export const getPostReduce = () => {
const postStream = new Bacon.Bus()
return {
input: postStream,
output: postStream
.doAction(logPostReduce)
}
}
Apply middleware
Middleware should be configured before importing any store.
Example of applying middlewares:
import * as Logger from 'bdux-logger'
import * as Timetravel from 'bdux-timetravel'
import { applyMiddleware } from 'bdux'
applyMiddleware(
Timetravel,
Logger
)