bdux v18.0.5
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
)
Examples
License
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago