Redux-Service

Middleware for Redux.

Offers a experimental way to handle (highly mutable || remote) data source && async logic in Redux

It's best to put all data you need inside the Redux State, and update in a Immutable way. But there are times the data you get just won't fit (remote), or best updated Mutable. And the logic & action for those data often need to be async.

It's best if you're familiar with the following:

And something nice to check out:

The Impression

Consider the basic Redux UI Loop:

Redux State gets updated ------------------------> UI render
pack Data in ActionObject <----------------------- User Interaction
dispatch ActionObject to Redux Store
Redux Store Reducer handles the Data
Redux State gets updated ------------------------> UI render
pack Data in ActionObject <----------------------- User Interaction
dispatch ActionObject to Redux Store
Redux Store Reducer handles the Data
Redux State gets updated ------------------------> UI render

The ActionObject accepts the input. And the Reducer applies the change. As simple as a Function call.

But also kind of limited: the Action is an Object, the Reducer is pure.

So the Middleware is introduced to fit more logic and control.

Redux-Service adds two layers of control:

  • Service, Generator-based, better async logic, also can handle mutable data
  • Entry, on top of Service, coordinate or filter Actions

Defining Service

A Service is a GeneratorFunction, called with ({ store, req, res }).

The result Generator may req(uest) Actions with specific Types, and res(ponse) to the store with other Actions

Service requires:

  • Session: Object Mutable for Data holding
  • Reducer: Redux Reducer push Session Object update to Redux Store

In ServiceGeneratorFunction, you can req(uest) Actions with specific Types, and res(ponse) to the store with other Actions

The Redux Reducer should repack the SessionObject every time and pass it to Redux Store. This ensures the SessionObject in Redux Store updates the Redux Immutable way. About SessionObject

Defining Entry

A Entry is a Function targeting a Specific Action Type.

When the Redux Store receives this Type of Action, the entryFunction is called with (store, action). With the Redux Store you can check State, dispatch some Actions.

Entry receives Actions before Service, and of course Redux Reducer. If the returned value of the entryFunction is true, the follow-up Redux Middleware or Redux Reducer will not receive this Action.

Action Process

when an Action comes through, the processing process will be like:

  • [some Redux Middleware]
  • Redux Middleware: Redux-Service
    • Entry: function ( store, action )
      • Check Redux State: store.getState()
      • Dispatch Actions: store.dispatch( action )
      • Block current Action: return true
    • Service: function * ({ store, req, res })
      • Check Redux State: store.getState()
      • Wait for Action Type: yield req( 'actionType' ) or yield req([ 'type', 'type' ])
      • Dispatch Actions: yield res( action )
  • [other Redux Middleware]
  • Redux Reducer
  • Redux State Update
About Action:

Dispatch the Action Object directly by:

  • Limit action format to { type: <String>, payload: <Object> }
  • Use namespace to prefix the Action Type, like: 'service:user:auth'
About ServiceGeneratorFunction:

Generator based SCP can make async logic easier to compose. Generator will pause at yield req( type or typeArray ), wait for expected Action to come and resume the logic. Generator will pause at yield res( action ), wait for the action gets dispatched and processed, then resume the logic.

Usage & Structure

One recommended file structure for a React + Redux + Redux-Service project:

project/
  containers/
    ...             ------ React Container & Component - UI render && Action dispatch
  reducers/
    ...             ------ Redux Reducer - Action consume, Mainly for UI-only data
  services/         ------ Async Data Logic || Mutable Data Source
    index.js        ------ Opinioned export style
    ServiceA.js
    ServiceB.js
  entry.js          ------ define all Entries
  configure.js      ------ configure Redux Store
  App.js            ------ The Root Component - Link Redux Provider to React

The files are mainly grouped by function.

  • project/containers/: Put React Container & Component here. Render Data to UI, then dispatch User interaction as ActionObjects. For details, please check: A Better File Structure For React/Redux Applications

  • project/reducers/: Mainly for UI-only data, since most External Data may be introduced to Redux Store from Service.

  • project/services/: Pack each Service, Session in separate file, but not Reducer. Instead, create then in a loop in index.js, like below:

// project/services/index.js
import { createSessionReducer } from 'redux-service'     
import ServiceA from './ServiceA'
import ServiceB from './ServiceB'

const reformList = [
  { ...ServiceA, name: 'service-a' },
  { ...ServiceB, name: 'service-b' }
]

const serviceMap = {}
const sessionMap = {}
const reducerMap = {}

reformList.forEach(({name, service, session}) => {
  serviceMap[name] = service
  sessionMap[name] = session
  reducerMap[name] = createSessionReducer(`reducer:${name}:update`, session)
})

export { serviceMap, sessionMap, reducerMap }
  • project/entry.js: Put all EntryFunctions in this file, if not that much.
export const entryMap = {
  'entry:type:a': (store, action) => {},
  'entry:type:b': (store, action) => {}
}
  • services/configure.js: Compose the Redux Store, by apply Reducer and Middleware.
import { createStore, applyMiddleware, combineReducers } from 'redux'
import ReduxService from 'redux-service'

import { entryMap } from './entry'
import { serviceMap, reducerMap as ServiceReducerMap } from './services'
import { reducerMap as UIReducerMap } from './reducers'

function configureReduxService () {
  const reduxService = new ReduxService()
  for (const key in entryMap) reduxService.setEntry(key, entryMap[ key ])
  for (const key in serviceMap) reduxService.setService(key, serviceMap[ key ])
  return reduxService
}

function configureReducer () {
  return combineReducers({
    service: combineReducers(ServiceReducerMap),
    ui: combineReducers(UIReducerMap)
  })
}

function configure () {
  const reduxService = configureReduxService()
  const store = createStore(
    configureReducer(),
    applyMiddleware(reduxService.middleware)
  )
  reduxService.startAllService()
  return {
    store,
    reduxService
  }
}

export default configure
  • services/App.js: Link the Codes:
import React from 'react'
import { Provider } from 'react-redux'

import configure from './configure'
import { ContainerA, ContainerB } from './containers'

// root Component, no redux on this level
export default class App extends React.Component {
  constructor () {
    super()
    this.store = null
    this.state = {
      isConfigureReady: false,
    }
  }

  componentWillMount () {
    configure((store) => {
      this.store = store
      this.store.dispatch({ type: 'entry:init' })
      this.setState({ isConfigureReady: true })
    })
  }

  componentWillUnmount () {
    this.store.dispatch({ type: 'entry:clear' })
  }

  render () {
    const { isConfigureReady } = this.state
    if (!isConfigureReady) return null
    return <Provider store={this.store}>
      <ContainerA />
      <ContainerB />
    </Provider>
  }
}

License

MIT

results matching ""

    No results matching ""