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' )
oryield 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 Applicationsproject/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 inindex.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