18.0.5 • Published 2 years ago

bdux v18.0.5

Weekly downloads
3
License
ISC
Repository
github
Last release
2 years ago

Bdux

A Flux architecture implementation out of enjoyment of Bacon.js, Redux and React.

Build Status Coverage Status Codacy Badge

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 as Pluggable 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 or skipDuplicates.
  • Should NOT have side effect. e.g. flatMap or throttle.

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 is map(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 a Pluggable stream to be applied before all reducers.
  • getPostReduce returns a Pluggable stream to be applied after reducers.
  • useHook is triggered in all components which include useBdux.

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

The ISC License

18.0.5

2 years ago

18.0.4

2 years ago

18.0.3

2 years ago

18.0.2

2 years ago

18.0.1

2 years ago

17.0.1

2 years ago

17.0.0

3 years ago

16.13.2

3 years ago

16.13.1

3 years ago

16.13.3

3 years ago

16.13.0

3 years ago

16.8.19

5 years ago

16.8.18

5 years ago

16.8.17

5 years ago

16.8.16

5 years ago

16.8.15

5 years ago

16.8.14

5 years ago

16.8.13

5 years ago

16.8.12

5 years ago

16.8.11

5 years ago

16.8.10

5 years ago

16.8.9

5 years ago

16.8.8

5 years ago

16.8.7

5 years ago

16.8.6

5 years ago

16.8.5

5 years ago

16.8.4

5 years ago

16.8.3

5 years ago

16.8.2

5 years ago

16.8.1

5 years ago

16.8.0

5 years ago

16.4.4

6 years ago

16.4.3

6 years ago

16.4.2

6 years ago

16.4.1

6 years ago

16.4.0

6 years ago

16.3.0

6 years ago

16.2.11

6 years ago

16.2.10

6 years ago

16.2.9

6 years ago

16.2.8

6 years ago

16.2.7

6 years ago

16.2.6

6 years ago

16.2.5

6 years ago

16.2.4

6 years ago

16.2.3

6 years ago

16.2.2

6 years ago

16.2.1

6 years ago

0.1.31

7 years ago

0.1.30

7 years ago

0.1.29

7 years ago

0.1.28

7 years ago

0.1.27

7 years ago

0.1.26

7 years ago

0.1.25

8 years ago

0.1.24

8 years ago

0.1.23

8 years ago

0.1.22

8 years ago

0.1.21

8 years ago

0.1.20

8 years ago

0.1.19

8 years ago

0.1.18

8 years ago

0.1.17

8 years ago

0.1.16

8 years ago

0.1.15

8 years ago

0.1.14

8 years ago

0.1.13

8 years ago

0.1.12

8 years ago

0.1.11

8 years ago

0.1.10

8 years ago

0.1.9

8 years ago

0.1.8

8 years ago

0.1.7

8 years ago

0.1.6

8 years ago

0.1.5

8 years ago

0.1.4

8 years ago

0.1.3

8 years ago

0.1.2

8 years ago

0.1.1

8 years ago

0.1.0

8 years ago