Skip to content

Commit

Permalink
feat(loadFeatureMiddleware): call each feature's init hook when it's …
Browse files Browse the repository at this point in the history
…added

this will be used by redux-features-hot-loader to activate hot reloading
  • Loading branch information
jedwards1211 committed Dec 18, 2016
1 parent 52eeb24 commit 9721225
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 3 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ store.dispatch(loadFeature('counter'))
})
```
## Reserved methods
The following (optional) methods are reserved in features:
* `load(store)`: called by `loadFeatureMiddleware` when a `loadFeature` action is dispatched. Returns a `Promise` that
will resolve to the new content for the feature (which will replace the existing feature in the state).
* `init(store)`: called by `loadFeatureMiddleware` when a feature is added via an `addFeature` action. This hook is
primarily intended to be used by `redux-features-hot-loader`.
## But Redux state shouldn't contain functions, React components, etc.!!
Yes, I understand. I thought about ways to store the functions, React components, etc. for features outside of
Expand Down Expand Up @@ -363,6 +371,7 @@ async function serverSideRender(request, response) {

const body = renderToString(app)

// get all state except features
const {features, ...initialState} = store.getState()

response.status(200).send(`<!DOCTYPE html>
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
"mindfront-redux-utils": "^1.2.0",
"mocha": "^3.2.0",
"nyc": "^10.0.0",
"pre-commit": "^1.1.3",
"redux": "^3.6.0",
"rimraf": "^2.5.4",
"semantic-release": "^6.3.2",
Expand Down
1 change: 1 addition & 0 deletions src/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type FeatureState = 'NOT_LOADED' | 'LOADING' | 'LOADED' | Error
export type FeatureStates = {[featureId: string]: FeatureState}

export type Feature<S, A> = {
init?: (store: MiddlewareAPI<S, A>) => any,
load?: (store: MiddlewareAPI<S, A>) => Promise<Feature<S, A>>,
middleware?: Middleware<S, A>,
reducer?: Reducer<S, A>,
Expand Down
10 changes: 9 additions & 1 deletion src/loadFeatureMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @flow

import type {Middleware, Feature, Features, FeatureStates, MiddlewareAPI, Dispatch, FeatureAction} from './index.js.flow'
import {LOAD_FEATURE, installFeature, setFeatureState, LOAD_INITIAL_FEATURES, loadFeature} from './actions'
import {ADD_FEATURE, LOAD_FEATURE, installFeature, setFeatureState, LOAD_INITIAL_FEATURES, loadFeature} from './actions'
import {defaultCreateMiddleware} from './defaults'

export default function loadFeatureMiddleware<S, A: {type: $Subtype<string>}>(
Expand All @@ -17,6 +17,14 @@ export default function loadFeatureMiddleware<S, A: {type: $Subtype<string>}>(
const createMiddleware = config.createMiddleware || defaultCreateMiddleware

return createMiddleware({
[ADD_FEATURE]: (store: MiddlewareAPI<S, A | FeatureAction>) => (next: Dispatch<A | FeatureAction>) => (action: any): any => {
const id = action.meta && action.meta.id
const priorFeature = (getFeatures(store.getState()) || {})[id]
const result = next(action)
const feature = (getFeatures(store.getState()) || {})[id]
if (!priorFeature && feature && feature.init instanceof Function) feature.init(store)
return result
},
[LOAD_FEATURE]: (store: MiddlewareAPI<S, A | FeatureAction>) => (next: Dispatch<A | FeatureAction>) => (action: any): any => {
const id = action.meta && action.meta.id
const featureStates = getFeatureStates(store.getState()) || {}
Expand Down
34 changes: 33 additions & 1 deletion test/loadFeatureMiddlewareTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {createMiddleware} from 'mindfront-redux-utils'
import featuresReducer from '../src/featuresReducer'
import featureStatesReducer from '../src/featureStatesReducer'
import loadFeatureMiddleware from '../src/loadFeatureMiddleware'
import {loadFeature, setFeatureState, installFeature, loadInitialFeatures} from '../src/actions'
import {addFeature, loadFeature, setFeatureState, installFeature, loadInitialFeatures} from '../src/actions'
import {expect} from 'chai'
import sinon from 'sinon'

Expand All @@ -19,6 +19,38 @@ describe('loadFeatureMiddleware', () => {
beforeEach(() => reducer.reset())

function tests(createTestStore) {
it("calls init on added features", () => {
const store = createStore(combineReducers({
featureStates: featureStatesReducer(),
features: featuresReducer(),
}), applyMiddleware(loadFeatureMiddleware()))

const init = sinon.spy()
const f1 = {init}

store.dispatch(addFeature('f1', f1))
expect(init.args[0][0].dispatch).to.be.an.instanceOf(Function)
expect(init.args[0][0].getState).to.be.an.instanceOf(Function)
})
it("doesn't call init on features that are already added", () => {
const init = sinon.spy()
const f1 = {init}

const store = createStore(combineReducers({
featureStates: featureStatesReducer(),
features: featuresReducer(),
}), {
featureStates: {
f1: 'NOT_LOADED',
},
features: {
f1,
},
}, applyMiddleware(loadFeatureMiddleware()))

store.dispatch(addFeature('f1', f1))
expect(init.called).to.be.false
})
it("returns rejected Promise for missing feature", () => {
let store = createTestStore()
return store.dispatch(loadFeature('f1', {}))
Expand Down

0 comments on commit 9721225

Please sign in to comment.