From 4c68696134f48a1ad71f62ce9aae3031c69ac0c5 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 9 Jul 2015 23:41:47 -0700 Subject: [PATCH 1/3] Add Flow type annotations --- .eslintrc | 4 ++++ .flowconfig | 9 +++++++++ src/Store.js | 26 +++++++++++++++++--------- src/createStore.js | 26 +++++++++++++++----------- src/middleware/thunk.js | 11 +++++++++-- src/types.js | 10 ++++++++++ src/utils/applyMiddleware.js | 16 +++++++++++----- src/utils/bindActionCreators.js | 8 +++++++- src/utils/combineReducers.js | 8 +++++--- src/utils/compose.js | 4 +++- src/utils/createStoreShape.js | 4 +++- src/utils/identity.js | 4 +++- src/utils/isPlainObject.js | 4 +++- src/utils/mapValues.js | 4 +++- src/utils/pick.js | 4 +++- src/utils/shallowEqual.js | 12 +++++++----- src/utils/shallowEqualScalar.js | 16 +++++++++------- 17 files changed, 121 insertions(+), 49 deletions(-) create mode 100644 .flowconfig create mode 100644 src/types.js diff --git a/.eslintrc b/.eslintrc index 5cf245ac59..90e3903312 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,10 @@ "react/jsx-uses-vars": 2, "react/react-in-jsx-scope": 2, + // Disable until Flow supports let and const + "no-var": 0, + "vars-on-top": 0, + //Temporarirly disabled due to a possible bug in babel-eslint (todomvc example) "block-scoped-var": 0, // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000000..369768b7ad --- /dev/null +++ b/.flowconfig @@ -0,0 +1,9 @@ +[ignore] +.*/lib +.*/test + +[include] + +[libs] + +[options] diff --git a/src/Store.js b/src/Store.js index ed85b987fc..5ee17c431d 100644 --- a/src/Store.js +++ b/src/Store.js @@ -1,8 +1,16 @@ +/* @flow */ + import invariant from 'invariant'; import isPlainObject from './utils/isPlainObject'; +import type { State, Action, Reducer } from './types'; + export default class Store { - constructor(reducer, initialState) { + state: State; + reducer: Reducer; + listeners: Array; + + constructor(reducer: Reducer, initialState: State): void { invariant( typeof reducer === 'function', 'Expected the reducer to be a function.' @@ -13,37 +21,37 @@ export default class Store { this.replaceReducer(reducer); } - getReducer() { + getReducer(): Reducer { return this.reducer; } - replaceReducer(nextReducer) { + replaceReducer(nextReducer: Reducer): void { this.reducer = nextReducer; this.dispatch({ type: '@@INIT' }); } - dispatch(action) { + dispatch(action: Action): Action { invariant( isPlainObject(action), 'Actions must be plain objects. Use custom middleware for async actions.' ); - const { reducer } = this; + var { reducer } = this; this.state = reducer(this.state, action); this.listeners.forEach(listener => listener()); return action; } - getState() { + getState(): State { return this.state; } - subscribe(listener) { - const { listeners } = this; + subscribe(listener: Function): Function { + var { listeners } = this; listeners.push(listener); return function unsubscribe() { - const index = listeners.indexOf(listener); + var index = listeners.indexOf(listener); listeners.splice(index, 1); }; } diff --git a/src/createStore.js b/src/createStore.js index 6a79406c98..1bfca57b2f 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,21 +1,25 @@ -import Store from './Store'; +/* @flow */ + +import StoreClass from './Store'; import combineReducers from './utils/combineReducers'; +import type { State, Action, Reducer, Dispatch, Store } from './types'; + export default function createStore( - reducer, - initialState -) { - const finalReducer = typeof reducer === 'function' ? + reducer: Reducer, + initialState: State +): Store { + var finalReducer = typeof reducer === 'function' ? reducer : combineReducers(reducer); - const store = new Store(finalReducer, initialState); + var store = new StoreClass(finalReducer, initialState); return { - dispatch: ::store.dispatch, - subscribe: ::store.subscribe, - getState: ::store.getState, - getReducer: ::store.getReducer, - replaceReducer: ::store.replaceReducer + dispatch: store.dispatch.bind(store), + subscribe: store.subscribe.bind(store), + getState: store.getState.bind(store), + getReducer: store.getReducer.bind(store), + replaceReducer: store.replaceReducer.bind(store) }; } diff --git a/src/middleware/thunk.js b/src/middleware/thunk.js index e6638cdb06..121aaa9bc9 100644 --- a/src/middleware/thunk.js +++ b/src/middleware/thunk.js @@ -1,5 +1,12 @@ -export default function thunkMiddleware({ dispatch, getState }) { - return next => action => +/* @flow */ + +import type { Dispatch, State, Action } from '../types'; + +type StoreMethods = { dispatch: Dispatch, getState: () => State }; + +export default function thunkMiddleware(storeMethods: StoreMethods): Dispatch { + var { dispatch, getState } = storeMethods; + return (next: Dispatch) => (action: Action) => typeof action === 'function' ? action(dispatch, getState) : next(action); diff --git a/src/types.js b/src/types.js new file mode 100644 index 0000000000..e1b79270d1 --- /dev/null +++ b/src/types.js @@ -0,0 +1,10 @@ +export type State = any; +export type Action = Object; +export type IntermediateAction = any; +export type Dispatch = (a: Action | IntermediateAction) => any; +export type Reducer = (state: S, action: A) => S; +export type ActionCreator = (...args: any) => Action | IntermediateAction +export type Middleware = (methods: { dispatch: Dispatch, getState: () => State }) => (next: Dispatch) => Dispatch; +export type Store = { dispatch: Dispatch, getState: State, subscribe: Function, getReducer: Reducer, replaceReducer: void }; +export type CreateStore = (reducer: Function, initialState: any) => Store; +export type HigherOrderStore = (next: CreateStore) => CreateStore; diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index e6c63ad0bb..a310afd63b 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,7 +1,11 @@ +/* @flow */ + import compose from './compose'; import composeMiddleware from './composeMiddleware'; import thunk from '../middleware/thunk'; +import type { Middleware, Dispatch, CreateStore } from '../types'; + /** * Creates a higher-order store that applies middleware to a store's dispatch. * Because middleware is potentially asynchronous, this should be the first @@ -9,14 +13,16 @@ import thunk from '../middleware/thunk'; * @param {...Function} ...middlewares * @return {Function} A higher-order store */ -export default function applyMiddleware(...middlewares) { - const finalMiddlewares = middlewares.length ? +export default function applyMiddleware( + ...middlewares: Array +): Dispatch { + var finalMiddlewares = middlewares.length ? middlewares : [thunk]; - return next => (...args) => { - const store = next(...args); - const methods = { + return (next: CreateStore) => (...args) => { + var store = next(...args); + var methods = { dispatch: store.dispatch, getState: store.getState }; diff --git a/src/utils/bindActionCreators.js b/src/utils/bindActionCreators.js index f9a81a5a80..05374c812c 100644 --- a/src/utils/bindActionCreators.js +++ b/src/utils/bindActionCreators.js @@ -1,6 +1,12 @@ +/* @flow */ + import mapValues from '../utils/mapValues'; -export default function bindActionCreators(actionCreators, dispatch) { +import type { Dispatch } from '../types'; + +export default function bindActionCreators( + actionCreators: Object, dispatch: Dispatch +): Object { return mapValues(actionCreators, actionCreator => (...args) => dispatch(actionCreator(...args)) ); diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index fe65c056cc..600e074e08 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -2,7 +2,9 @@ import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; import invariant from 'invariant'; -function getErrorMessage(key, action) { +import type { Action, State, Reducer } from '../types'; + +function getErrorMessage(key: String, action: Action): String { const actionType = action && action.type; const actionName = actionType && `"${actionType}"` || 'an action'; const reducerName = `Reducer "${key}"`; @@ -21,10 +23,10 @@ function getErrorMessage(key, action) { ); } -export default function combineReducers(reducers) { +export default function combineReducers(reducers: Object): Reducer { const finalReducers = pick(reducers, (val) => typeof val === 'function'); - return function composition(state = {}, action) { + return function composition(state: State = {}, action: Action): State { return mapValues(finalReducers, (reducer, key) => { const newState = reducer(state[key], action); invariant( diff --git a/src/utils/compose.js b/src/utils/compose.js index 4db0884d3b..b4b291e015 100644 --- a/src/utils/compose.js +++ b/src/utils/compose.js @@ -1,8 +1,10 @@ +/* @flow */ + /** * Composes functions from left to right * @param {...Function} funcs - Functions to compose * @return {Function} */ -export default function compose(...funcs) { +export default function compose(...funcs: Array): Function { return funcs.reduceRight((composed, f) => f(composed)); } diff --git a/src/utils/createStoreShape.js b/src/utils/createStoreShape.js index 851e7ce898..f4e22159ed 100644 --- a/src/utils/createStoreShape.js +++ b/src/utils/createStoreShape.js @@ -1,4 +1,6 @@ -export default function createStoreShape(PropTypes) { +/* @flow */ + +export default function createStoreShape(PropTypes: Object): Object { return PropTypes.shape({ subscribe: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, diff --git a/src/utils/identity.js b/src/utils/identity.js index 8c690a8859..ccab53fd9a 100644 --- a/src/utils/identity.js +++ b/src/utils/identity.js @@ -1,3 +1,5 @@ -export default function identity(value) { +/* @flow */ + +export default function identity(value: T): T { return value; } diff --git a/src/utils/isPlainObject.js b/src/utils/isPlainObject.js index a5845486cf..b31d8f37a3 100644 --- a/src/utils/isPlainObject.js +++ b/src/utils/isPlainObject.js @@ -1,3 +1,5 @@ -export default function isPlainObject(obj) { +/* @flow */ + +export default function isPlainObject(obj: Object): boolean { return obj ? typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype : false; } diff --git a/src/utils/mapValues.js b/src/utils/mapValues.js index 29d203cf61..9e09aeaab8 100644 --- a/src/utils/mapValues.js +++ b/src/utils/mapValues.js @@ -1,4 +1,6 @@ -export default function mapValues(obj, fn) { +/* @flow */ + +export default function mapValues(obj: Object, fn: Function): Object { return Object.keys(obj).reduce((result, key) => { result[key] = fn(obj[key], key); return result; diff --git a/src/utils/pick.js b/src/utils/pick.js index 2c9719c1c0..7025cb35e3 100644 --- a/src/utils/pick.js +++ b/src/utils/pick.js @@ -1,4 +1,6 @@ -export default function pick(obj, fn) { +/* @flow */ + +export default function pick(obj: Object, fn: Function): Object { return Object.keys(obj).reduce((result, key) => { if (fn(obj[key])) { result[key] = obj[key]; diff --git a/src/utils/shallowEqual.js b/src/utils/shallowEqual.js index f82be71949..8414ec3be1 100644 --- a/src/utils/shallowEqual.js +++ b/src/utils/shallowEqual.js @@ -1,18 +1,20 @@ -export default function shallowEqual(objA, objB) { +/* @flow */ + +export default function shallowEqual(objA: Object, objB: Object): boolean { if (objA === objB) { return true; } - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); + var keysA = Object.keys(objA); + var keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. - const hasOwn = Object.prototype.hasOwnProperty; - for (let i = 0; i < keysA.length; i++) { + var hasOwn = Object.prototype.hasOwnProperty; + for (var i = 0; i < keysA.length; i++) { if (!hasOwn.call(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) { return false; diff --git a/src/utils/shallowEqualScalar.js b/src/utils/shallowEqualScalar.js index 2adb8ea85b..8f4f779a03 100644 --- a/src/utils/shallowEqualScalar.js +++ b/src/utils/shallowEqualScalar.js @@ -1,4 +1,6 @@ -export default function shallowEqualScalar(objA, objB) { +/* @flow */ + +export default function shallowEqualScalar(objA: Object, objB: Object): boolean { if (objA === objB) { return true; } @@ -8,22 +10,22 @@ export default function shallowEqualScalar(objA, objB) { return false; } - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); + var keysA = Object.keys(objA); + var keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. - const hasOwn = Object.prototype.hasOwnProperty; - for (let i = 0; i < keysA.length; i++) { + var hasOwn = Object.prototype.hasOwnProperty; + for (var i = 0; i < keysA.length; i++) { if (!hasOwn.call(objB, keysA[i])) { return false; } - const valA = objA[keysA[i]]; - const valB = objB[keysA[i]]; + var valA = objA[keysA[i]]; + var valB = objB[keysA[i]]; if (valA !== valB || typeof valA === 'object' || typeof valB === 'object') { return false; From e85461771a28a47638e57a171b2f43c8dbef4eb9 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 9 Jul 2015 23:44:57 -0700 Subject: [PATCH 2/3] Fix linting --- src/createStore.js | 2 +- src/utils/applyMiddleware.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/createStore.js b/src/createStore.js index 1bfca57b2f..5268cf5b27 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -3,7 +3,7 @@ import StoreClass from './Store'; import combineReducers from './utils/combineReducers'; -import type { State, Action, Reducer, Dispatch, Store } from './types'; +import type { State, Reducer, Store } from './types'; export default function createStore( reducer: Reducer, diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index a310afd63b..a147d863f3 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -4,7 +4,7 @@ import compose from './compose'; import composeMiddleware from './composeMiddleware'; import thunk from '../middleware/thunk'; -import type { Middleware, Dispatch, CreateStore } from '../types'; +import type { Dispatch, CreateStore } from '../types'; /** * Creates a higher-order store that applies middleware to a store's dispatch. From ac8a1841fbdae5f6c94363583cdfaaf792ffba90 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 19 Jul 2015 22:07:25 +0300 Subject: [PATCH 3/3] Fix the missing Flow annotations --- src/createStore.js | 20 +++++++-------- src/index.js | 4 +-- src/types.js | 40 ++++++++++++++++++++++++++---- src/utils/applyMiddleware.js | 22 ++++++++++------ src/utils/combineReducers.js | 14 +++++------ src/utils/composeMiddleware.js | 10 +++++--- test/utils/applyMiddleware.spec.js | 4 +-- 7 files changed, 76 insertions(+), 38 deletions(-) diff --git a/src/createStore.js b/src/createStore.js index 4bb312d8f5..9e116a1ae9 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,6 +1,6 @@ /* @flow */ /*eslint-disable */ -import type { State, Reducer, Store } from './types'; +import type { State, Reducer, Action, IntermediateAction, Store } from './types'; /*eslint-enable */ import invariant from 'invariant'; @@ -9,7 +9,7 @@ import isPlainObject from './utils/isPlainObject'; // Don't ever try to handle these action types in your code. They are private. // For any unknown actions, you must return the current state. // If the current state is undefined, you must return the initial state. -export const ActionTypes = { +export var ActionTypes = { INIT: '@@redux/INIT' }; @@ -22,24 +22,24 @@ export default function createStore( 'Expected the reducer to be a function.' ); - let currentReducer = null; - let currentState = initialState; - let listeners = []; + var currentReducer = reducer; + var currentState = initialState; + var listeners = []; function getState() { return currentState; } - function subscribe(listener) { + function subscribe(listener: Function) { listeners.push(listener); return function unsubscribe() { - const index = listeners.indexOf(listener); + var index = listeners.indexOf(listener); listeners.splice(index, 1); }; } - function dispatch(action) { + function dispatch(action: Action) { invariant( isPlainObject(action), 'Actions must be plain objects. Use custom middleware for async actions.' @@ -54,12 +54,12 @@ export default function createStore( return currentReducer; } - function replaceReducer(nextReducer) { + function replaceReducer(nextReducer: Reducer) { currentReducer = nextReducer; dispatch({ type: ActionTypes.INIT }); } - replaceReducer(reducer); + dispatch({ type: ActionTypes.INIT }); return { dispatch, diff --git a/src/index.js b/src/index.js index 1cfbbf6489..dd0cdd5f11 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ -// Core +/* @flow */ + import createStore from './createStore'; -// Utilities import compose from './utils/compose'; import combineReducers from './utils/combineReducers'; import bindActionCreators from './utils/bindActionCreators'; diff --git a/src/types.js b/src/types.js index fb4c576fbd..740c1b039f 100644 --- a/src/types.js +++ b/src/types.js @@ -1,10 +1,40 @@ +/* @flow */ + export type State = any; + export type Action = Object; + export type IntermediateAction = any; + export type Dispatch = (a: Action | IntermediateAction) => any; + export type Reducer = (state: S, action: A) => S; -export type ActionCreator = (...args: any) => Action | IntermediateAction; -export type Middleware = (methods: { dispatch: Dispatch, getState: () => State }) => (next: Dispatch) => Dispatch; -export type Store = { dispatch: Dispatch, getState: State, subscribe: Function, getReducer: Reducer, replaceReducer: void }; -export type CreateStore = (reducer: Function, initialState: any) => Store; -export type HigherOrderStore = (next: CreateStore) => CreateStore; + +export type ActionCreator = (...args: any) => + Action | IntermediateAction; + +export type MiddlewareArgs = { + dispatch: Dispatch; + getState: () => State; +}; + +export type Middleware = (args: MiddlewareArgs) => + (next: Dispatch) => + Dispatch; + +export type Store = { + dispatch: Dispatch; + getState: () => State; + getReducer: Reducer; + replaceReducer: (nextReducer: Reducer) => void; + subscribe: (listener: () => void) => () => void; +}; + +export type CreateStore = ( + reducer: Reducer, + initialState: State +) => Store; + +export type HigherOrderStore = ( + next: CreateStore +) => CreateStore; diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index 2fb49c31b5..00140faf27 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,6 +1,9 @@ /* @flow */ /*eslint-disable */ -import type { Dispatch, CreateStore, Middleware } from '../types'; +import type { + Dispatch, Middleware, Reducer, State, + Store, CreateStore, HigherOrderStore +} from '../types'; /*eslint-enable */ import compose from './compose'; @@ -15,22 +18,25 @@ import composeMiddleware from './composeMiddleware'; */ export default function applyMiddleware( ...middlewares: Array -): CreateStore { - return (next: CreateStore) => (reducer, initialState) => { +): HigherOrderStore { + return (next: CreateStore) => (reducer: Reducer, initialState: State) => { var store = next(reducer, initialState); var middleware = composeMiddleware(...middlewares); - var composedDispatch = null; + var composedDispatch = () => {}; function dispatch(action) { return composedDispatch(action); } - var methods = { - dispatch, - getState: store.getState + var middlewareAPI = { + getState: store.getState, + dispatch }; - composedDispatch = compose(middleware(methods), store.dispatch); + composedDispatch = compose( + middleware(middlewareAPI), + store.dispatch + ); return { ...store, diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index 5f823b1ca2..54a74a4c82 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -8,9 +8,9 @@ import pick from '../utils/pick'; import invariant from 'invariant'; import { ActionTypes } from '../createStore'; -function getErrorMessage(key: String, action: Action): String { - const actionType = action && action.type; - const actionName = actionType && `"${actionType}"` || 'an action'; +function getErrorMessage(key: String, action: Action): string { + var actionType = action && action.type; + var actionName = actionType && `"${actionType}"` || 'an action'; return ( `Reducer "${key}" returned undefined handling ${actionName}. ` + @@ -19,10 +19,10 @@ function getErrorMessage(key: String, action: Action): String { } export default function combineReducers(reducers: Object): Reducer { - const finalReducers = pick(reducers, (val) => typeof val === 'function'); + var finalReducers = pick(reducers, (val) => typeof val === 'function'); Object.keys(finalReducers).forEach(key => { - const reducer = finalReducers[key]; + var reducer = finalReducers[key]; invariant( typeof reducer(undefined, { type: ActionTypes.INIT }) !== 'undefined', `Reducer "${key}" returned undefined during initialization. ` + @@ -31,7 +31,7 @@ export default function combineReducers(reducers: Object): Reducer { `not be undefined.` ); - const type = Math.random().toString(36).substring(7).split('').join('.'); + var type = Math.random().toString(36).substring(7).split('').join('.'); invariant( typeof reducer(undefined, { type }) !== 'undefined', `Reducer "${key}" returned undefined when probed with a random type. ` + @@ -45,7 +45,7 @@ export default function combineReducers(reducers: Object): Reducer { return function composition(state: State = {}, action: Action): State { return mapValues(finalReducers, (reducer, key) => { - const newState = reducer(state[key], action); + var newState = reducer(state[key], action); invariant( typeof newState !== 'undefined', getErrorMessage(key, action) diff --git a/src/utils/composeMiddleware.js b/src/utils/composeMiddleware.js index a3680d3b5c..8eee92b28c 100644 --- a/src/utils/composeMiddleware.js +++ b/src/utils/composeMiddleware.js @@ -1,7 +1,6 @@ /* @flow */ /*eslint-disable */ -import type { Dispatch, Middleware } from '../types'; -type StoreMethods = { dispatch: Dispatch, getState: () => State }; +import type { Dispatch, Middleware, MiddlewareArgs } from '../types'; /*eslint-enable */ import compose from './compose'; @@ -14,6 +13,9 @@ import compose from './compose'; export default function composeMiddleware( ...middlewares: Array ): Middleware { - return (methods: StoreMethods) => (next: Dispatch) => - compose(...middlewares.map(m => m(methods)), next); + return (args: MiddlewareArgs) => (next: Dispatch) => { + var dispatchChain = middlewares.map(middleware => middleware(args)); + dispatchChain.push(next); + return compose.apply(null, dispatchChain); + }; } diff --git a/test/utils/applyMiddleware.spec.js b/test/utils/applyMiddleware.spec.js index d3f8f60ea5..0ca73d84c0 100644 --- a/test/utils/applyMiddleware.spec.js +++ b/test/utils/applyMiddleware.spec.js @@ -22,8 +22,8 @@ describe('applyMiddleware', () => { expect(spy.calls.length).toEqual(1); expect(Object.keys(spy.calls[0].arguments[0])).toEqual([ - 'dispatch', - 'getState' + 'getState', + 'dispatch' ]); expect(store.getState()).toEqual([ { id: 1, text: 'Use Redux' }, { id: 2, text: 'Flux FTW!' } ]);