Skip to content

Flow types, take 3 (actually 4) #254

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

Merged
merged 5 commits into from
Jul 19, 2015
Merged
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
4 changes: 4 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[ignore]
.*/lib
.*/test

[include]

[libs]

[options]
28 changes: 18 additions & 10 deletions src/createStore.js
Original file line number Diff line number Diff line change
@@ -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.'
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
40 changes: 40 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -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<S, A> = (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;
32 changes: 22 additions & 10 deletions src/utils/applyMiddleware.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<Middleware>
): 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,
Expand Down
10 changes: 9 additions & 1 deletion src/utils/bindActionCreators.js
Original file line number Diff line number Diff line change
@@ -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))
);
Expand Down
23 changes: 14 additions & 9 deletions src/utils/combineReducers.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
/* @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}. ` +
`To ignore an action, you must explicitly return the previous state.`
);
}

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. ` +
Expand All @@ -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. ` +
Expand All @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion src/utils/compose.js
Original file line number Diff line number Diff line change
@@ -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>): Function {
return funcs.reduceRight((composed, f) => f(composed));
}
15 changes: 13 additions & 2 deletions src/utils/composeMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
/* @flow */
/*eslint-disable */
import type { Dispatch, Middleware, MiddlewareArgs } from '../types';
/*eslint-enable */

import compose from './compose';

/**
* Compose middleware from left to right
* @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>
): Middleware {
return (args: MiddlewareArgs) => (next: Dispatch) => {
var dispatchChain = middlewares.map(middleware => middleware(args));
dispatchChain.push(next);
return compose.apply(null, dispatchChain);
};
}
11 changes: 9 additions & 2 deletions src/utils/isPlainObject.js
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 3 additions & 1 deletion src/utils/mapValues.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/utils/pick.js
Original file line number Diff line number Diff line change
@@ -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];
Expand Down
4 changes: 2 additions & 2 deletions test/utils/applyMiddleware.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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!' } ]);
Expand Down