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 bduxAction
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 = {}).
namespecifies a unique store name, which can be:- A string.
- A function
props => ({ name }).
getReducerreturns a reducer asPluggablewhich is an object contains the input and output of a stream.otherStoresis 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.
scanorskipDuplicates. - Should NOT have side effect. e.g.
flatMaporthrottle.
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).
storesis an object of dependent stores.callbacksis any array of functions to be triggered after subscribing to stores.skipDuplicatesis a function to map store properties. The default behaviour ismap(property => property.skipDuplicates()).
The hooks return an object of:
stateis an object of the current values of stores.dispatchis a function to dispatch the return value of an action creator to stores.bindToDispatchbinds 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.
getPreReducereturns aPluggablestream to be applied before all reducers.getPostReducereturns aPluggablestream to be applied after reducers.useHookis 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