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/createStore.js b/src/createStore.js index 78ab81f1d7..9e116a1ae9 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,37 +1,45 @@ +/* @flow */ +/*eslint-disable */ +import type { State, Reducer, Action, IntermediateAction, Store } from './types'; +/*eslint-enable */ + import invariant from 'invariant'; 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' }; -export default function createStore(reducer, initialState) { +export default function createStore( + reducer: Reducer, + initialState: State +): Store { invariant( typeof reducer === 'function', '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.' @@ -46,12 +54,12 @@ export default function createStore(reducer, initialState) { 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 new file mode 100644 index 0000000000..740c1b039f --- /dev/null +++ b/src/types.js @@ -0,0 +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 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 ac1434260a..00140faf27 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,3 +1,11 @@ +/* @flow */ +/*eslint-disable */ +import type { + Dispatch, Middleware, Reducer, State, + Store, CreateStore, HigherOrderStore +} from '../types'; +/*eslint-enable */ + import compose from './compose'; import composeMiddleware from './composeMiddleware'; @@ -8,23 +16,27 @@ import composeMiddleware from './composeMiddleware'; * @param {...Function} ...middlewares * @return {Function} A higher-order store */ -export default function applyMiddleware(...middlewares) { - return next => (...args) => { - const store = next(...args); - const middleware = composeMiddleware(...middlewares); - - let composedDispatch = null; +export default function applyMiddleware( + ...middlewares: Array +): HigherOrderStore { + return (next: CreateStore) => (reducer: Reducer, initialState: State) => { + var store = next(reducer, initialState); + var middleware = composeMiddleware(...middlewares); + var composedDispatch = () => {}; function dispatch(action) { return composedDispatch(action); } - const 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/bindActionCreators.js b/src/utils/bindActionCreators.js index f9a81a5a80..0775b9d27a 100644 --- a/src/utils/bindActionCreators.js +++ b/src/utils/bindActionCreators.js @@ -1,6 +1,14 @@ +/* @flow */ +/*eslint-disable */ +import type { Dispatch } from '../types'; +/*eslint-enable */ + import mapValues from '../utils/mapValues'; -export default function bindActionCreators(actionCreators, dispatch) { +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 dbf6c966ec..54a74a4c82 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -1,11 +1,16 @@ +/* @flow */ +/*eslint-disable */ +import type { Action, State, Reducer } from '../types'; +/*eslint-enable */ + import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; import invariant from 'invariant'; import { ActionTypes } from '../createStore'; -function getErrorMessage(key, action) { - 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}. ` + @@ -13,11 +18,11 @@ function getErrorMessage(key, action) { ); } -export default function combineReducers(reducers) { - const finalReducers = pick(reducers, (val) => typeof val === 'function'); +export default function combineReducers(reducers: Object): Reducer { + 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. ` + @@ -26,7 +31,7 @@ export default function combineReducers(reducers) { `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. ` + @@ -38,9 +43,9 @@ export default function combineReducers(reducers) { ); }); - return function composition(state = {}, action) { + 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/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/composeMiddleware.js b/src/utils/composeMiddleware.js index bf702aae30..8eee92b28c 100644 --- a/src/utils/composeMiddleware.js +++ b/src/utils/composeMiddleware.js @@ -1,3 +1,8 @@ +/* @flow */ +/*eslint-disable */ +import type { Dispatch, Middleware, MiddlewareArgs } from '../types'; +/*eslint-enable */ + import compose from './compose'; /** @@ -5,6 +10,12 @@ import compose from './compose'; * @param {...Function} middlewares * @return {Function} */ -export default function composeMiddleware(...middlewares) { - return methods => next => compose(...middlewares.map(m => m(methods)), next); +export default function composeMiddleware( + ...middlewares: Array +): Middleware { + return (args: MiddlewareArgs) => (next: Dispatch) => { + var dispatchChain = middlewares.map(middleware => middleware(args)); + dispatchChain.push(next); + return compose.apply(null, dispatchChain); + }; } diff --git a/src/utils/isPlainObject.js b/src/utils/isPlainObject.js index a5845486cf..b352d71181 100644 --- a/src/utils/isPlainObject.js +++ b/src/utils/isPlainObject.js @@ -1,3 +1,10 @@ -export default function isPlainObject(obj) { - return obj ? typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype : false; +/* @flow */ + +export default function isPlainObject(obj: Object): boolean { + if (!obj) { + return false; + } + + return typeof obj === 'object' && + Object.getPrototypeOf(obj) === Object.prototype; } 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/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!' } ]);