Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternate Proof of Concept: Enhancer Overhaul #2214

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/advanced/Middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,16 @@ The implementation of [`applyMiddleware()`](../api/applyMiddleware.md) that ship

* It only exposes a subset of the [store API](../api/Store.md) to the middleware: [`dispatch(action)`](../api/Store.md#dispatch) and [`getState()`](../api/Store.md#getState).

* It does a bit of trickery to make sure that if you call `store.dispatch(action)` from your middleware instead of `next(action)`, the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware, as we have seen [previously](AsyncActions.md).
* It does a bit of trickery to make sure that if you call `store.dispatch(action)` from your middleware instead of `next(action)`, the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware, as we have seen [previously](AsyncActions.md). There is one caveat when calling `dispatch` during setup, described below.

* To ensure that you may only apply middleware once, it operates on `createStore()` rather than on `store` itself. Instead of `(store, middlewares) => store`, its signature is `(...middlewares) => (createStore) => createStore`.

Because it is cumbersome to apply functions to `createStore()` before using it, `createStore()` accepts an optional last argument to specify such functions.

#### Caveat: Dispatching During Setup

While `applyMiddleware` executes and sets up your middleware, the `store.dispatch` function will point to the vanilla version provided by `createStore`. Dispatching would result in no other middleware being applied. If you are expecting an interaction with another middleware during setup, you will probably be disappointed. Because of this unexpected behavior, `applyMiddleware` will throw an error if you try to dispatch an action before the set up completes. Instead, you should either communicate directly with that other middleware via a common object (for an API-calling middleware, this may be your API client object) or waiting until after the middleware is constructed with a callback.

### The Final Approach

Given this middleware we just wrote:
Expand Down
8 changes: 4 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ export interface Action {
*
* @template S State object type.
*/
export type Reducer<S> = <A extends Action>(state: S, action: A) => S;
export type Reducer<S> = <A extends Action>(state: S | undefined, action: A) => S;

/**
* Object whose values correspond to different reducer functions.
*/
export interface ReducersMapObject {
[key: string]: Reducer<any>;
export type ReducersMapObject<S> = {
[K in keyof S]: Reducer<S[K]>;
}

/**
Expand All @@ -70,7 +70,7 @@ export interface ReducersMapObject {
* @returns A reducer function that invokes every reducer inside the passed
* object, and builds a state object with the same shape.
*/
export function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>;
export function combineReducers<S>(reducers: ReducersMapObject<S>): Reducer<S>;


/* store */
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@
"jest": "^18.0.0",
"rimraf": "^2.3.4",
"rxjs": "^5.0.0-beta.6",
"typescript": "^1.8.0",
"typescript-definition-tester": "0.0.4",
"typescript": "^2.1.0",
"typescript-definition-tester": "0.0.5",
"webpack": "^1.9.6"
},
"npmName": "redux",
Expand Down
44 changes: 44 additions & 0 deletions src/adaptEnhancer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { validateModernEnhancers } from './createStore'

// Transforms a modernEnhancer (store => partialStore) into a classicEnhancer
// (createStore => (reducer, preloadedState) => store) by wrapping it in an
// adapter function. This is to maintain backwards compatibility with the
// classic way of composing store enhancers with the `compose` function.
export default function adaptEnhancer(modernEnhancer) {
if (typeof modernEnhancer !== 'function') {
throw new Error(`Expected 'modernEnhancer' to be a function.`)
}

function enhancer(createStore) {
const length = createStore.length
if (length !== 4) {
throw new Error(
`Expected 'createStore' to accept 4 arguments but it accepts ${length}.`
)
}

return (reducer, preloadedState, classicEnhancer, modernEnhancers) => {
validateModernEnhancers(modernEnhancers)

return createStore(
reducer,
preloadedState,
classicEnhancer,
(modernEnhancers || []).concat(modernEnhancer)
)
}
}

enhancer.modern = modernEnhancer
return enhancer
}

// Since most store enhancers have a factory function, this adapter function is
// provided as a convenience. See `applyMiddleware` as an example.
export function adaptEnhancerCreator(createModernEnhancer) {
if (typeof createModernEnhancer !== 'function') {
throw new Error(`Expected 'createModernEnhancer' to be a function.`)
}

return (...args) => adaptEnhancer(createModernEnhancer(...args))
}
58 changes: 41 additions & 17 deletions src/applyMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import compose from './compose'
import { adaptEnhancerCreator } from './adaptEnhancer'

export default adaptEnhancerCreator(applyMiddleware)

/**
* Creates a store enhancer that applies middleware to the dispatch method
Expand All @@ -10,28 +12,50 @@ import compose from './compose'
* Because middleware is potentially asynchronous, this should be the first
* store enhancer in the composition chain.
*
* Note that each middleware will be given the `dispatch` and `getState` functions
* as named arguments.
* Note that each middleware will be given the `dispatch` and `getState`
* functions as named arguments.
*
* @param {...Function} middlewares The middleware chain to be applied.
* @param {...Function} middleware The middleware chain to be applied.
* @returns {Function} A store enhancer applying the middleware.
*/
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
function applyMiddleware(...middleware) {
return store => {
const dispatchProxy = createDispatchProxy()

const middlewareAPI = {
const api = {
getState: store.getState,
dispatch: (action) => dispatch(action)
dispatch: dispatchProxy.dispatch,
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
const dispatch = middleware
.map(mid => mid(api))
.reduceRight((next, mid) => mid(next), store.dispatch)

dispatchProxy.replace(dispatch)
return { dispatch }
}
}

// Because the `finalDispatch` function isn't known until after the middleware
// have been composed, but it needs to be accessible to those middleware before
// then, it needs to be wrapped in a proxy function. To prevent that function
// from capturing `middleware` for the lifetime of the running application, it
// is defined outside of the `applyMiddleware` function.
function createDispatchProxy() {
let finalDispatch = throwPrematureDispatch
return {
dispatch(...args) {
return finalDispatch(...args)
},
replace(newDispatch) {
finalDispatch = newDispatch
},
}
}

function throwPrematureDispatch() {
throw new Error(
'Dispatching while constructing your middleware is not allowed. Other'
+ ' middleware would not be applied to this dispatch.'
)
}
2 changes: 1 addition & 1 deletion src/combineReducers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionTypes } from './createStore'
import ActionTypes from './utils/actionTypes'
import isPlainObject from 'lodash/isPlainObject'
import warning from './utils/warning'

Expand Down
41 changes: 41 additions & 0 deletions src/createEvent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export default function createEvent() {
let currentListeners = []
let nextListeners = currentListeners

function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}

function invoke() {
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
}

function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}

let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)

return function unsubscribe() {
if (!isSubscribed) {
return
}

isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}

return { invoke, subscribe }
}
Loading