18.0.5 • Published 3 years ago

bdux v18.0.5

Weekly downloads
3
License
ISC
Repository
github
Last release
3 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

3 years ago

18.0.4

3 years ago

18.0.3

3 years ago

18.0.2

3 years ago

18.0.1

3 years ago

17.0.1

4 years ago

17.0.0

4 years ago

16.13.2

4 years ago

16.13.1

4 years ago

16.13.3

4 years ago

16.13.0

4 years ago

16.8.19

6 years ago

16.8.18

6 years ago

16.8.17

6 years ago

16.8.16

6 years ago

16.8.15

6 years ago

16.8.14

6 years ago

16.8.13

6 years ago

16.8.12

6 years ago

16.8.11

6 years ago

16.8.10

6 years ago

16.8.9

6 years ago

16.8.8

6 years ago

16.8.7

7 years ago

16.8.6

7 years ago

16.8.5

7 years ago

16.8.4

7 years ago

16.8.3

7 years ago

16.8.2

7 years ago

16.8.1

7 years ago

16.8.0

7 years ago

16.4.4

7 years ago

16.4.3

7 years ago

16.4.2

7 years ago

16.4.1

7 years ago

16.4.0

7 years ago

16.3.0

8 years ago

16.2.11

8 years ago

16.2.10

8 years ago

16.2.9

8 years ago

16.2.8

8 years ago

16.2.7

8 years ago

16.2.6

8 years ago

16.2.5

8 years ago

16.2.4

8 years ago

16.2.3

8 years ago

16.2.2

8 years ago

16.2.1

8 years ago

0.1.31

8 years ago

0.1.30

8 years ago

0.1.29

8 years ago

0.1.28

8 years ago

0.1.27

9 years ago

0.1.26

9 years ago

0.1.25

9 years ago

0.1.24

9 years ago

0.1.23

9 years ago

0.1.22

9 years ago

0.1.21

9 years ago

0.1.20

9 years ago

0.1.19

9 years ago

0.1.18

9 years ago

0.1.17

9 years ago

0.1.16

9 years ago

0.1.15

10 years ago

0.1.14

10 years ago

0.1.13

10 years ago

0.1.12

10 years ago

0.1.11

10 years ago

0.1.10

10 years ago

0.1.9

10 years ago

0.1.8

10 years ago

0.1.7

10 years ago

0.1.6

10 years ago

0.1.5

10 years ago

0.1.4

10 years ago

0.1.3

10 years ago

0.1.2

10 years ago

0.1.1

10 years ago

0.1.0

10 years ago