From 6b5c7560883652764bd426e545075d411906408c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 28 Jan 2016 02:29:25 +0000 Subject: [PATCH 1/5] Add first-class support for store enhancers to createStore() API --- examples/async/store/configureStore.js | 11 +++++------ examples/real-world/store/configureStore.dev.js | 13 +++++++------ examples/real-world/store/configureStore.prod.js | 12 ++++++------ examples/shopping-cart/index.js | 6 ++++-- examples/universal/common/store/configureStore.js | 10 +++++----- src/createStore.js | 11 ++++++++++- 6 files changed, 37 insertions(+), 26 deletions(-) diff --git a/examples/async/store/configureStore.js b/examples/async/store/configureStore.js index f826a8b6ba..95133af831 100644 --- a/examples/async/store/configureStore.js +++ b/examples/async/store/configureStore.js @@ -3,13 +3,12 @@ import thunkMiddleware from 'redux-thunk' import createLogger from 'redux-logger' import rootReducer from '../reducers' -const createStoreWithMiddleware = applyMiddleware( - thunkMiddleware, - createLogger() -)(createStore) - export default function configureStore(initialState) { - const store = createStoreWithMiddleware(rootReducer, initialState) + const store = createStore( + rootReducer, + initialState, + applyMiddleware(thunkMiddleware, createLogger()), + ) if (module.hot) { // Enable Webpack hot module replacement for reducers diff --git a/examples/real-world/store/configureStore.dev.js b/examples/real-world/store/configureStore.dev.js index e5a1da4151..45745b38da 100644 --- a/examples/real-world/store/configureStore.dev.js +++ b/examples/real-world/store/configureStore.dev.js @@ -1,4 +1,4 @@ -import { createStore, applyMiddleware, compose } from 'redux' +import { createStore, applyMiddleware } from 'redux' import { syncHistory } from 'react-router-redux' import { browserHistory } from 'react-router' import DevTools from '../containers/DevTools' @@ -8,13 +8,14 @@ import createLogger from 'redux-logger' import rootReducer from '../reducers' const reduxRouterMiddleware = syncHistory(browserHistory) -const finalCreateStore = compose( - applyMiddleware(thunk, api, reduxRouterMiddleware, createLogger()), - DevTools.instrument() -)(createStore) export default function configureStore(initialState) { - const store = finalCreateStore(rootReducer, initialState) + const store = createStore( + rootReducer, + initialState, + applyMiddleware(thunk, api, reduxRouterMiddleware, createLogger()), + DevTools.instrument() + ) // Required for replaying actions from devtools to work reduxRouterMiddleware.listenForReplays(store) diff --git a/examples/real-world/store/configureStore.prod.js b/examples/real-world/store/configureStore.prod.js index d1e00f523b..a45088f0e0 100644 --- a/examples/real-world/store/configureStore.prod.js +++ b/examples/real-world/store/configureStore.prod.js @@ -1,14 +1,14 @@ -import { createStore, applyMiddleware, compose } from 'redux' +import { createStore, applyMiddleware } from 'redux' import { syncHistory } from 'react-router-redux' import { browserHistory } from 'react-router' import thunk from 'redux-thunk' import api from '../middleware/api' import rootReducer from '../reducers' -const finalCreateStore = compose( - applyMiddleware(thunk, api, syncHistory(browserHistory)), -)(createStore) - export default function configureStore(initialState) { - return finalCreateStore(rootReducer, initialState) + return createStore( + rootReducer, + initialState, + applyMiddleware(thunk, api, syncHistory(browserHistory)) + ) } diff --git a/examples/shopping-cart/index.js b/examples/shopping-cart/index.js index 5a92fd55b9..a5ed64c548 100644 --- a/examples/shopping-cart/index.js +++ b/examples/shopping-cart/index.js @@ -12,8 +12,10 @@ const middleware = process.env.NODE_ENV === 'production' ? [ thunk ] : [ thunk, logger() ] -const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore) -const store = createStoreWithMiddleware(reducer) +const store = createStore( + reducer, + applyMiddleware(...middleware) +) store.dispatch(getAllProducts()) diff --git a/examples/universal/common/store/configureStore.js b/examples/universal/common/store/configureStore.js index 8fa225d592..b570589174 100644 --- a/examples/universal/common/store/configureStore.js +++ b/examples/universal/common/store/configureStore.js @@ -2,12 +2,12 @@ import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import rootReducer from '../reducers' -const createStoreWithMiddleware = applyMiddleware( - thunk -)(createStore) - export default function configureStore(initialState) { - const store = createStoreWithMiddleware(rootReducer, initialState) + const store = createStore( + rootReducer, + initialState, + applyMiddleware(thunk) + ) if (module.hot) { // Enable Webpack hot module replacement for reducers diff --git a/src/createStore.js b/src/createStore.js index 3528b1d0c5..97341c3e52 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,4 +1,5 @@ import isPlainObject from './utils/isPlainObject' +import compose from './compose' /** * These are private action types reserved by Redux. @@ -30,7 +31,15 @@ export var ActionTypes = { * @returns {Store} A Redux store that lets you read the state, dispatch actions * and subscribe to changes. */ -export default function createStore(reducer, initialState) { +export default function createStore(reducer, initialState, ...enhancers) { + if (typeof initialState === 'function') { + enhancers.unshift(initialState) + initialState = undefined + } + if (enhancers.length > 0) { + return compose(...enhancers)(createStore)(reducer, initialState) + } + if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } From a876af26a682d4cf4977e3e30482b97a8223e50b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 28 Jan 2016 03:31:24 +0000 Subject: [PATCH 2/5] Update to the proposal by @timdorr --- .../real-world/store/configureStore.dev.js | 8 ++++--- src/createStore.js | 21 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/examples/real-world/store/configureStore.dev.js b/examples/real-world/store/configureStore.dev.js index 45745b38da..2e9e027c64 100644 --- a/examples/real-world/store/configureStore.dev.js +++ b/examples/real-world/store/configureStore.dev.js @@ -1,4 +1,4 @@ -import { createStore, applyMiddleware } from 'redux' +import { createStore, applyMiddleware, compose } from 'redux' import { syncHistory } from 'react-router-redux' import { browserHistory } from 'react-router' import DevTools from '../containers/DevTools' @@ -13,8 +13,10 @@ export default function configureStore(initialState) { const store = createStore( rootReducer, initialState, - applyMiddleware(thunk, api, reduxRouterMiddleware, createLogger()), - DevTools.instrument() + compose( + applyMiddleware(thunk, api, reduxRouterMiddleware, createLogger()), + DevTools.instrument() + ) ) // Required for replaying actions from devtools to work diff --git a/src/createStore.js b/src/createStore.js index 97341c3e52..71a4030f50 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,5 +1,4 @@ import isPlainObject from './utils/isPlainObject' -import compose from './compose' /** * These are private action types reserved by Redux. @@ -28,16 +27,26 @@ export var ActionTypes = { * If you use `combineReducers` to produce the root reducer function, this must be * an object with the same shape as `combineReducers` keys. * + * @param {Function} enhancer The store enhancer. You may optionally specify it + * to enhance the store with third-party capabilities such as the middleware, + * time travel, persistence, etc. The only store enhancer that ships with Redux + * is `applyMiddleware()`. + * * @returns {Store} A Redux store that lets you read the state, dispatch actions * and subscribe to changes. */ -export default function createStore(reducer, initialState, ...enhancers) { - if (typeof initialState === 'function') { - enhancers.unshift(initialState) +export default function createStore(reducer, initialState, enhancer) { + if (typeof initialState === 'function' && typeof enhancer === 'undefined') { + enhancer = initialState initialState = undefined } - if (enhancers.length > 0) { - return compose(...enhancers)(createStore)(reducer, initialState) + + if (typeof enhancer !== 'undefined') { + if (typeof enhancer !== 'function') { + throw new Error('Expected the enhancer to be a function.') + } + + return enhancer(createStore)(reducer, initialState) } if (typeof reducer !== 'function') { From 147842cf9d80902287a41f7129bfadfea8afb112 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 28 Jan 2016 16:32:57 +0000 Subject: [PATCH 3/5] Add tests for the new createStore() enhancer arg --- test/createStore.spec.js | 89 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/test/createStore.spec.js b/test/createStore.spec.js index 11b1c4e00b..65d5e04acb 100644 --- a/test/createStore.spec.js +++ b/test/createStore.spec.js @@ -15,7 +15,7 @@ describe('createStore', () => { expect(methods).toContain('replaceReducer') }) - it('requires a reducer function', () => { + it('throws if reducer is not a function', () => { expect(() => createStore() ).toThrow() @@ -449,4 +449,91 @@ describe('createStore', () => { store.dispatch({ type: '' }) ).toNotThrow() }) + + it('accepts enhancer as the third argument', () => { + const emptyArray = [] + const spyEnhancer = vanillaCreateStore => (...args) => { + expect(args[0]).toBe(reducers.todos) + expect(args[1]).toBe(emptyArray) + expect(args.length).toBe(2) + const vanillaStore = vanillaCreateStore(...args) + return { + ...vanillaStore, + dispatch: expect.createSpy(vanillaStore.dispatch).andCallThrough() + } + } + + const store = createStore(reducers.todos, emptyArray, spyEnhancer) + const action = addTodo('Hello') + store.dispatch(action) + expect(store.dispatch).toHaveBeenCalledWith(action) + expect(store.getState()).toEqual([ + { + id: 1, + text: 'Hello' + } + ]) + }) + + it('accepts enhancer as the second argument if initial state is missing', () => { + const spyEnhancer = vanillaCreateStore => (...args) => { + expect(args[0]).toBe(reducers.todos) + expect(args[1]).toBe(undefined) + expect(args.length).toBe(2) + const vanillaStore = vanillaCreateStore(...args) + return { + ...vanillaStore, + dispatch: expect.createSpy(vanillaStore.dispatch).andCallThrough() + } + } + + const store = createStore(reducers.todos, spyEnhancer) + const action = addTodo('Hello') + store.dispatch(action) + expect(store.dispatch).toHaveBeenCalledWith(action) + expect(store.getState()).toEqual([ + { + id: 1, + text: 'Hello' + } + ]) + }) + + it('throws if enhancer is neither undefined nor a function', () => { + expect(() => + createStore(reducers.todos, undefined, {}) + ).toThrow() + + expect(() => + createStore(reducers.todos, undefined, []) + ).toThrow() + + expect(() => + createStore(reducers.todos, undefined, null) + ).toThrow() + + expect(() => + createStore(reducers.todos, undefined, false) + ).toThrow() + + expect(() => + createStore(reducers.todos, undefined, undefined) + ).toNotThrow() + + expect(() => + createStore(reducers.todos, undefined, x => x) + ).toNotThrow() + + expect(() => + createStore(reducers.todos, x => x) + ).toNotThrow() + + expect(() => + createStore(reducers.todos, []) + ).toNotThrow() + + expect(() => + createStore(reducers.todos, {}) + ).toNotThrow() + }) }) From dc5fdb4ae66950f358a7d9504ab4819aff9bc79b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 28 Jan 2016 17:01:21 +0000 Subject: [PATCH 4/5] Update docs to use the simplified enhancer application --- docs/advanced/AsyncActions.md | 17 ++++++------ docs/advanced/ExampleRedditAPI.md | 14 +++++----- docs/advanced/Middleware.md | 39 ++++++++++++++------------- docs/api/applyMiddleware.md | 25 +++++++++++------- docs/api/compose.md | 44 +++++++------------------------ docs/api/createStore.md | 4 +++ src/createStore.js | 2 +- 7 files changed, 69 insertions(+), 76 deletions(-) diff --git a/docs/advanced/AsyncActions.md b/docs/advanced/AsyncActions.md index 5a9a34df49..44cb30ea58 100644 --- a/docs/advanced/AsyncActions.md +++ b/docs/advanced/AsyncActions.md @@ -138,7 +138,7 @@ Here’s what the state shape for our “Reddit headlines” app might look like { id: 42, title: 'Confusion about Flux and Relay' - }, + }, { id: 500, title: 'Creating a Simple Application Using React JS and Flux Architecture' @@ -384,7 +384,7 @@ export function fetchPosts(subreddit) { >import 'babel-core/polyfill' >``` -How do we include the Redux Thunk middleware in the dispatch mechanism? We use the [`applyMiddleware()`](../api/applyMiddleware.md) method from Redux, as shown below: +How do we include the Redux Thunk middleware in the dispatch mechanism? We use the [`applyMiddleware()`](../api/applyMiddleware.md) store enhancer from Redux, as shown below: #### `index.js` @@ -397,12 +397,13 @@ import rootReducer from './reducers' const loggerMiddleware = createLogger() -const createStoreWithMiddleware = applyMiddleware( - thunkMiddleware, // lets us dispatch() functions - loggerMiddleware // neat middleware that logs actions -)(createStore) - -const store = createStoreWithMiddleware(rootReducer) +const store = createStore( + rootReducer, + applyMiddleware( + thunkMiddleware, // lets us dispatch() functions + loggerMiddleware // neat middleware that logs actions + ) +) store.dispatch(selectSubreddit('reactjs')) store.dispatch(fetchPosts('reactjs')).then(() => diff --git a/docs/advanced/ExampleRedditAPI.md b/docs/advanced/ExampleRedditAPI.md index 21677b27ed..a3b76d6f29 100644 --- a/docs/advanced/ExampleRedditAPI.md +++ b/docs/advanced/ExampleRedditAPI.md @@ -170,13 +170,15 @@ import rootReducer from './reducers' const loggerMiddleware = createLogger() -const createStoreWithMiddleware = applyMiddleware( - thunkMiddleware, - loggerMiddleware -)(createStore) - export default function configureStore(initialState) { - return createStoreWithMiddleware(rootReducer, initialState) + return createStore( + rootReducer, + initialState, + applyMiddleware( + thunkMiddleware, + loggerMiddleware + ) + ) } ``` diff --git a/docs/advanced/Middleware.md b/docs/advanced/Middleware.md index 620cc1c5d2..badc968e4e 100644 --- a/docs/advanced/Middleware.md +++ b/docs/advanced/Middleware.md @@ -272,6 +272,8 @@ The implementation of [`applyMiddleware()`](../api/applyMiddleware.md) that ship * 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. + ### The Final Approach Given this middleware we just wrote: @@ -305,13 +307,12 @@ Here’s how to apply it to a Redux store: ```js import { createStore, combineReducers, applyMiddleware } from 'redux' -// applyMiddleware takes createStore() and returns -// a function with a compatible API. -let createStoreWithMiddleware = applyMiddleware(logger, crashReporter)(createStore) - -// Use it like you would use createStore() let todoApp = combineReducers(reducers) -let store = createStoreWithMiddleware(todoApp) +let store = createStore( + todoApp, + // applyMiddleware() tells createStore() how to handle middleware + applyMiddleware(logger, crashReporter) +) ``` That’s it! Now any actions dispatched to the store instance will flow through `logger` and `crashReporter`: @@ -378,8 +379,8 @@ const timeoutScheduler = store => next => action => { } /** - * Schedules actions with { meta: { raf: true } } to be dispatched inside a rAF loop - * frame. Makes `dispatch` return a function to remove the action from the queue in + * Schedules actions with { meta: { raf: true } } to be dispatched inside a rAF loop + * frame. Makes `dispatch` return a function to remove the action from the queue in * this case. */ const rafScheduler = store => next => { @@ -472,15 +473,17 @@ const thunk = store => next => action => // You can use all of them! (It doesn’t mean you should.) -let createStoreWithMiddleware = applyMiddleware( - rafScheduler, - timeoutScheduler, - thunk, - vanillaPromise, - readyStatePromise, - logger, - crashReporter -)(createStore) let todoApp = combineReducers(reducers) -let store = createStoreWithMiddleware(todoApp) +let store = createStore( + todoApp, + applyMiddleware( + rafScheduler, + timeoutScheduler, + thunk, + vanillaPromise, + readyStatePromise, + logger, + crashReporter + ) +) ``` diff --git a/docs/api/applyMiddleware.md b/docs/api/applyMiddleware.md index 29dcdd2633..abdb3f142f 100644 --- a/docs/api/applyMiddleware.md +++ b/docs/api/applyMiddleware.md @@ -14,7 +14,7 @@ Middleware is not baked into [`createStore`](createStore.md) and is not a fundam #### Returns -(*Function*) A store enhancer that applies the given middleware. The store enhancer is a function that needs to be applied to `createStore`. It will return a different `createStore` which has the middleware enabled. +(*Function*) A store enhancer that applies the given middleware. The store enhancer signature is `createStore => createStore'` but the easiest way to apply it is to pass it to [`createStore()`](./createStore.md) as the last `enhancer` argument. #### Example: Custom Logger Middleware @@ -37,8 +37,11 @@ function logger({ getState }) { } } -let createStoreWithMiddleware = applyMiddleware(logger)(createStore) -let store = createStoreWithMiddleware(todos, [ 'Use Redux' ]) +let store = createStore( + todos, + [ 'Use Redux' ], + applyMiddleware(logger) +) store.dispatch({ type: 'ADD_TODO', @@ -56,12 +59,9 @@ import { createStore, combineReducers, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import * as reducers from './reducers' -// applyMiddleware supercharges createStore with middleware: -let createStoreWithMiddleware = applyMiddleware(thunk)(createStore) - -// We can use it exactly like “vanilla” createStore. let reducer = combineReducers(reducers) -let store = createStoreWithMiddleware(reducer) +// applyMiddleware supercharges createStore with middleware: +let store = createStore(reducer, applyMiddleware(thunk)) function fetchSecretSauce() { return fetch('https://www.google.com/search?q=secret+sauce') @@ -229,7 +229,12 @@ export default connect( let d = require('another-debug-middleware'); middleware = [ ...middleware, c, d ]; } - const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore); + + const store = createStore( + reducer, + initialState, + applyMiddleware(middleware) + ) ``` This makes it easier for bundling tools to cut out unneeded modules and reduces the size of your builds. @@ -237,3 +242,5 @@ export default connect( * Ever wondered what `applyMiddleware` itself is? It ought to be an extension mechanism more powerful than the middleware itself. Indeed, `applyMiddleware` is an example of the most powerful Redux extension mechanism called [store enhancers](../Glossary.md#store-enhancer). It is highly unlikely you’ll ever want to write a store enhancer yourself. Another example of a store enhancer is [redux-devtools](https://github.com/gaearon/redux-devtools). Middleware is less powerful than a store enhancer, but it is easier to write. * Middleware sounds much more complicated than it really is. The only way to really understand middleware is to see how the existing middleware works, and try to write your own. The function nesting can be intimidating, but most of the middleware you’ll find are, in fact, 10-liners, and the nesting and composability is what makes the middleware system powerful. + +* To apply multiple store enhancers, you may use [`compose()`](./compose.md). diff --git a/docs/api/compose.md b/docs/api/compose.md index fb6e0b7e09..c524d84407 100644 --- a/docs/api/compose.md +++ b/docs/api/compose.md @@ -20,40 +20,16 @@ This example demonstrates how to use `compose` to enhance a [store](Store.md) wi ```js import { createStore, combineReducers, applyMiddleware, compose } from 'redux' import thunk from 'redux-thunk' -import * as reducers from '../reducers/index' - -let reducer = combineReducers(reducers) -let middleware = [ thunk ] - -let finalCreateStore - -// In production, we want to use just the middleware. -// In development, we want to use some store enhancers from redux-devtools. -// UglifyJS will eliminate the dead code depending on the build environment. - -if (process.env.NODE_ENV === 'production') { - finalCreateStore = applyMiddleware(...middleware)(createStore) -} else { - finalCreateStore = compose( - applyMiddleware(...middleware), - require('redux-devtools').devTools(), - require('redux-devtools').persistState( - window.location.href.match(/[?&]debug_session=([^&]+)\b/) - ) - )(createStore) - - // Same code without the `compose` helper: - // - // finalCreateStore = applyMiddleware(middleware)( - // require('redux-devtools').devTools()( - // require('redux-devtools').persistState( - // window.location.href.match(/[?&]debug_session=([^&]+)\b/) - // )(createStore) - // ) - // ) -} - -let store = finalCreateStore(reducer) +import DevTools from './containers/DevTools' +import reducer from '../reducers/index' + +const store = createStore( + reducer, + compose( + applyMiddleware(thunk), + DevTools.instrument() + ) +) ``` #### Tips diff --git a/docs/api/createStore.md b/docs/api/createStore.md index 97c86fcd77..f3086bea96 100644 --- a/docs/api/createStore.md +++ b/docs/api/createStore.md @@ -9,6 +9,8 @@ There should only be a single store in your app. 2. [`initialState`] *(any)*: The initial state. You may optionally specify it to hydrate the state from the server in universal apps, or to restore a previously serialized user session. If you produced `reducer` with [`combineReducers`](combineReducers.md), this must be a plain object with the same shape as the keys passed to it. Otherwise, you are free to pass anything that your `reducer` can understand. +3. [`enhancer`] *(Function)*: The store enhancer. You may optionally specify it to enhance the store with third-party capabilities such as middleware, time travel, persistence, etc. The only store enhancer that ships with Redux is [`applyMiddleware()`](./applyMiddleware.md). + #### Returns ([*`Store`*](Store.md)): An object that holds the complete state of your app. The only way to change its state is by [dispatching actions](Store.md#dispatch). You may also [subscribe](Store.md#subscribe) to the changes to its state to update the UI. @@ -49,3 +51,5 @@ console.log(store.getState()) * For universal apps that run on the server, create a store instance with every request so that they are isolated. Dispatch a few data fetching actions to a store instance and wait for them to complete before rendering the app on the server. * When a store is created, Redux dispatches a dummy action to your reducer to populate the store with the initial state. You are not meant to handle the dummy action directly. Just remember that your reducer should return some kind of initial state if the state given to it as the first argument is `undefined`, and you’re all set. + +* To apply multiple store enhancers, you may use [`compose()`](./compose.md). diff --git a/src/createStore.js b/src/createStore.js index 71a4030f50..082fad8ec0 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -28,7 +28,7 @@ export var ActionTypes = { * an object with the same shape as `combineReducers` keys. * * @param {Function} enhancer The store enhancer. You may optionally specify it - * to enhance the store with third-party capabilities such as the middleware, + * to enhance the store with third-party capabilities such as middleware, * time travel, persistence, etc. The only store enhancer that ships with Redux * is `applyMiddleware()`. * From 5a1b3e045f624fc3e96701c4e9b7193a49accfdd Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 28 Jan 2016 17:07:38 +0000 Subject: [PATCH 5/5] Remove dangling comma --- examples/async/store/configureStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/async/store/configureStore.js b/examples/async/store/configureStore.js index 95133af831..d821edebc9 100644 --- a/examples/async/store/configureStore.js +++ b/examples/async/store/configureStore.js @@ -7,7 +7,7 @@ export default function configureStore(initialState) { const store = createStore( rootReducer, initialState, - applyMiddleware(thunkMiddleware, createLogger()), + applyMiddleware(thunkMiddleware, createLogger()) ) if (module.hot) {