diff --git a/examples/todos-flow/.flowconfig b/examples/todos-flow/.flowconfig new file mode 100644 index 0000000000..50732a402f --- /dev/null +++ b/examples/todos-flow/.flowconfig @@ -0,0 +1,9 @@ +[ignore] +/node_modules/fbjs + +[include] + +[libs] +../../flow-typed + +[options] diff --git a/examples/todos-flow/.gitignore b/examples/todos-flow/.gitignore new file mode 100644 index 0000000000..8e8b40aff5 --- /dev/null +++ b/examples/todos-flow/.gitignore @@ -0,0 +1,11 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules + +# production +build + +# misc +.DS_Store +npm-debug.log diff --git a/examples/todos-flow/README.md b/examples/todos-flow/README.md new file mode 100644 index 0000000000..ddaced00d1 --- /dev/null +++ b/examples/todos-flow/README.md @@ -0,0 +1,34 @@ +# Redux Todos Example + +This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `npm run build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + diff --git a/examples/todos-flow/index.html b/examples/todos-flow/index.html new file mode 100644 index 0000000000..2e474bce12 --- /dev/null +++ b/examples/todos-flow/index.html @@ -0,0 +1,21 @@ + + + + + + Redux Todos Example + + +
+ + + diff --git a/examples/todos-flow/package.json b/examples/todos-flow/package.json new file mode 100644 index 0000000000..9ecd5d3a02 --- /dev/null +++ b/examples/todos-flow/package.json @@ -0,0 +1,25 @@ +{ + "name": "todos", + "version": "0.0.1", + "private": true, + "devDependencies": { + "enzyme": "^2.4.1", + "react-addons-test-utils": "^15.3.0", + "react-scripts": "^0.4.0" + }, + "dependencies": { + "react": "^15.3.0", + "react-dom": "^15.3.0", + "react-redux": "^4.4.5", + "redux": "^3.5.2" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "eject": "react-scripts eject", + "test": "react-scripts test" + }, + "eslintConfig": { + "extends": "./node_modules/react-scripts/config/eslint.js" + } +} diff --git a/examples/todos-flow/src/actions/index.js b/examples/todos-flow/src/actions/index.js new file mode 100644 index 0000000000..0224081f22 --- /dev/null +++ b/examples/todos-flow/src/actions/index.js @@ -0,0 +1,26 @@ +// @flow +import type { Id, Text, VisibilityFilter, Action } from '../types' + +let nextTodoId: Id = 0 + +export const addTodo = (text: Text): Action => { + return { + type: 'ADD_TODO', + id: nextTodoId++, + text + } +} + +export const setVisibilityFilter = (filter: VisibilityFilter): Action => { + return { + type: 'SET_VISIBILITY_FILTER', + filter + } +} + +export const toggleTodo = (id: Id): Action => { + return { + type: 'TOGGLE_TODO', + id + } +} diff --git a/examples/todos-flow/src/actions/index.spec.js b/examples/todos-flow/src/actions/index.spec.js new file mode 100644 index 0000000000..37596590ec --- /dev/null +++ b/examples/todos-flow/src/actions/index.spec.js @@ -0,0 +1,25 @@ +import * as actions from './index' + +describe('todo actions', () => { + it('addTodo should create ADD_TODO action', () => { + expect(actions.addTodo('Use Redux')).toEqual({ + type: 'ADD_TODO', + id: 0, + text: 'Use Redux' + }) + }) + + it('setVisibilityFilter should create SET_VISIBILITY_FILTER action', () => { + expect(actions.setVisibilityFilter('active')).toEqual({ + type: 'SET_VISIBILITY_FILTER', + filter: 'active' + }) + }) + + it('toggleTodo should create TOGGLE_TODO action', () => { + expect(actions.toggleTodo(1)).toEqual({ + type: 'TOGGLE_TODO', + id: 1 + }) + }) +}) diff --git a/examples/todos-flow/src/components/App.js b/examples/todos-flow/src/components/App.js new file mode 100644 index 0000000000..c63582fccb --- /dev/null +++ b/examples/todos-flow/src/components/App.js @@ -0,0 +1,15 @@ +// @flow +import React from 'react' +import Footer from './Footer' +import AddTodo from '../containers/AddTodo' +import VisibleTodoList from '../containers/VisibleTodoList' + +const App = () => ( +
+ + +
+
+) + +export default App diff --git a/examples/todos-flow/src/components/Footer.js b/examples/todos-flow/src/components/Footer.js new file mode 100644 index 0000000000..abfa1beb4f --- /dev/null +++ b/examples/todos-flow/src/components/Footer.js @@ -0,0 +1,23 @@ +// @flow +import React from 'react' +import FilterLink from '../containers/FilterLink' + +const Footer = () => ( +

+ Show: + {" "} + + All + + {", "} + + Active + + {", "} + + Completed + +

+) + +export default Footer diff --git a/examples/todos-flow/src/components/Link.js b/examples/todos-flow/src/components/Link.js new file mode 100644 index 0000000000..3076379e3f --- /dev/null +++ b/examples/todos-flow/src/components/Link.js @@ -0,0 +1,27 @@ +// @flow +import React from 'react' + +export type Props = { + active: boolean, + children?: React$Element, + onClick: () => void +}; + +const Link = ({ active, children, onClick }: Props) => { + if (active) { + return {children} + } + + return ( + { + e.preventDefault() + onClick() + }} + > + {children} + + ) +} + +export default Link diff --git a/examples/todos-flow/src/components/Todo.js b/examples/todos-flow/src/components/Todo.js new file mode 100644 index 0000000000..4b37f3a80a --- /dev/null +++ b/examples/todos-flow/src/components/Todo.js @@ -0,0 +1,22 @@ +// @flow +import React from 'react' +import type { Text } from '../types' + +export type Props = { + onClick: () => void, + completed: boolean, + text: Text +}; + +const Todo = ({ onClick, completed, text }: Props) => ( +
  • + {text} +
  • +) + +export default Todo diff --git a/examples/todos-flow/src/components/TodoList.js b/examples/todos-flow/src/components/TodoList.js new file mode 100644 index 0000000000..74d69f2fd9 --- /dev/null +++ b/examples/todos-flow/src/components/TodoList.js @@ -0,0 +1,23 @@ +// @flow +import React from 'react' +import Todo from './Todo' +import type { Todos, Id } from '../types' + +export type Props = { + todos: Todos, + onTodoClick: (id: Id) => void +}; + +const TodoList = ({ todos, onTodoClick }: Props) => ( + +) + +export default TodoList diff --git a/examples/todos-flow/src/containers/AddTodo.js b/examples/todos-flow/src/containers/AddTodo.js new file mode 100644 index 0000000000..8f6beee25a --- /dev/null +++ b/examples/todos-flow/src/containers/AddTodo.js @@ -0,0 +1,38 @@ +// @flow +import React from 'react' +import { connect } from 'react-redux' +import { addTodo } from '../actions' +import type { Dispatch } from '../types' +import type { Connector } from 'react-redux' + +type Props = { + dispatch: Dispatch +}; + +const AddTodo = ({ dispatch }) => { + let input + + return ( +
    +
    { + e.preventDefault() + if (!input.value.trim()) { + return + } + dispatch(addTodo(input.value)) + input.value = '' + }}> + { + input = node + }} /> + +
    +
    + ) +} + +const connector: Connector<{}, Props> = connect() + +export default connector(AddTodo) diff --git a/examples/todos-flow/src/containers/FilterLink.js b/examples/todos-flow/src/containers/FilterLink.js new file mode 100644 index 0000000000..530c326ee4 --- /dev/null +++ b/examples/todos-flow/src/containers/FilterLink.js @@ -0,0 +1,32 @@ +// @flow +import { connect } from 'react-redux' +import { setVisibilityFilter } from '../actions' +import Link from '../components/Link' +import type { Props } from '../components/Link' +import type { State, Dispatch, VisibilityFilter } from '../types' +import type { Connector } from 'react-redux' + +type OwnProps = { + filter: VisibilityFilter +}; + +const mapStateToProps = (state: State, ownProps) => { + return { + active: ownProps.filter === state.visibilityFilter + } +} + +const mapDispatchToProps = (dispatch: Dispatch, ownProps) => { + return { + onClick: () => { + dispatch(setVisibilityFilter(ownProps.filter)) + } + } +} + +const connector: Connector = connect( + mapStateToProps, + mapDispatchToProps +) + +export default connector(Link) diff --git a/examples/todos-flow/src/containers/VisibleTodoList.js b/examples/todos-flow/src/containers/VisibleTodoList.js new file mode 100644 index 0000000000..177153dbc3 --- /dev/null +++ b/examples/todos-flow/src/containers/VisibleTodoList.js @@ -0,0 +1,40 @@ +// @flow +import { connect } from 'react-redux' +import { toggleTodo } from '../actions' +import TodoList from '../components/TodoList' +import type { State, Dispatch } from '../types' +import type { Connector } from 'react-redux' +import type { Props } from '../components/TodoList' + +const getVisibleTodos = (todos, filter) => { + switch (filter) { + case 'SHOW_COMPLETED': + return todos.filter(t => t.completed) + case 'SHOW_ACTIVE': + return todos.filter(t => !t.completed) + case 'SHOW_ALL': + default : + return todos + } +} + +const mapStateToProps = (state: State) => { + return { + todos: getVisibleTodos(state.todos, state.visibilityFilter) + } +} + +const mapDispatchToProps = (dispatch: Dispatch) => { + return { + onTodoClick: (id) => { + dispatch(toggleTodo(id)) + } + } +} + +const connector: Connector<{}, Props> = connect( + mapStateToProps, + mapDispatchToProps +) + +export default connector(TodoList) diff --git a/examples/todos-flow/src/index.js b/examples/todos-flow/src/index.js new file mode 100644 index 0000000000..a37f989125 --- /dev/null +++ b/examples/todos-flow/src/index.js @@ -0,0 +1,17 @@ +// @flow +import React from 'react' +import { render } from 'react-dom' +import { createStore } from 'redux' +import { Provider } from 'react-redux' +import App from './components/App' +import reducer from './reducers' +import type { Store } from './types' + +const store: Store = createStore(reducer) + +render( + + + , + document.getElementById('root') +) diff --git a/examples/todos-flow/src/reducers/index.js b/examples/todos-flow/src/reducers/index.js new file mode 100644 index 0000000000..31bd283729 --- /dev/null +++ b/examples/todos-flow/src/reducers/index.js @@ -0,0 +1,12 @@ +// @flow +import todos from './todos' +import visibilityFilter from './visibilityFilter' +import type { State, Action } from '../types' + +export default function todoApp(state: ?State, action: Action): State { + const s = state || {} + return { + todos: todos(s.todos, action), + visibilityFilter: visibilityFilter(s.visibilityFilter, action) + } +} diff --git a/examples/todos-flow/src/reducers/todos.js b/examples/todos-flow/src/reducers/todos.js new file mode 100644 index 0000000000..0a0e553976 --- /dev/null +++ b/examples/todos-flow/src/reducers/todos.js @@ -0,0 +1,37 @@ +// @flow +import type { Todos, Todo, Id, Text, Action } from '../types' + +function createTodo(id: Id, text: Text): Todo { + return { + id, + text, + completed: false + } +} + +function toggleTodo(todos: Todos, id: Id): Todos { + return todos.map(t => { + if (t.id !== id) { + return t + } + return Object.assign({}, t, { + completed: !t.completed + }) + }) +} + +const todos = (state: Todos = [], action: Action): Todos => { + switch (action.type) { + case 'ADD_TODO': + return [ + ...state, + createTodo(action.id, action.text) + ] + case 'TOGGLE_TODO': + return toggleTodo(state, action.id) + default: + return state + } +} + +export default todos diff --git a/examples/todos-flow/src/reducers/todos.spec.js b/examples/todos-flow/src/reducers/todos.spec.js new file mode 100644 index 0000000000..88eca35d78 --- /dev/null +++ b/examples/todos-flow/src/reducers/todos.spec.js @@ -0,0 +1,111 @@ +import todos from './todos' + +describe('todos reducer', () => { + it('should handle initial state', () => { + expect( + todos(undefined, {}) + ).toEqual([]) + }) + + it('should handle ADD_TODO', () => { + expect( + todos([], { + type: 'ADD_TODO', + text: 'Run the tests', + id: 0 + }) + ).toEqual([ + { + text: 'Run the tests', + completed: false, + id: 0 + } + ]) + + expect( + todos([ + { + text: 'Run the tests', + completed: false, + id: 0 + } + ], { + type: 'ADD_TODO', + text: 'Use Redux', + id: 1 + }) + ).toEqual([ + { + text: 'Run the tests', + completed: false, + id: 0 + }, { + text: 'Use Redux', + completed: false, + id: 1 + } + ]) + + expect( + todos([ + { + text: 'Run the tests', + completed: false, + id: 0 + }, { + text: 'Use Redux', + completed: false, + id: 1 + } + ], { + type: 'ADD_TODO', + text: 'Fix the tests', + id: 2 + }) + ).toEqual([ + { + text: 'Run the tests', + completed: false, + id: 0 + }, { + text: 'Use Redux', + completed: false, + id: 1 + }, { + text: 'Fix the tests', + completed: false, + id: 2 + } + ]) + }) + + it('should handle TOGGLE_TODO', () => { + expect( + todos([ + { + text: 'Run the tests', + completed: false, + id: 1 + }, { + text: 'Use Redux', + completed: false, + id: 0 + } + ], { + type: 'TOGGLE_TODO', + id: 1 + }) + ).toEqual([ + { + text: 'Run the tests', + completed: true, + id: 1 + }, { + text: 'Use Redux', + completed: false, + id: 0 + } + ]) + }) + +}) diff --git a/examples/todos-flow/src/reducers/visibilityFilter.js b/examples/todos-flow/src/reducers/visibilityFilter.js new file mode 100644 index 0000000000..d69614d53d --- /dev/null +++ b/examples/todos-flow/src/reducers/visibilityFilter.js @@ -0,0 +1,13 @@ +// @flow +import type { VisibilityFilter, Action } from '../types' + +const visibilityFilter = (state: VisibilityFilter = 'SHOW_ALL', action: Action): VisibilityFilter => { + switch (action.type) { + case 'SET_VISIBILITY_FILTER': + return action.filter + default: + return state + } +} + +export default visibilityFilter diff --git a/examples/todos-flow/src/types/index.js b/examples/todos-flow/src/types/index.js new file mode 100644 index 0000000000..64ccc5ccd8 --- /dev/null +++ b/examples/todos-flow/src/types/index.js @@ -0,0 +1,35 @@ +// @flow +import type { Store as ReduxStore, Dispatch as ReduxDispatch } from 'redux' + +export type Id = number; + +export type Text = string; + +export type Todo = { + id: Id, + text: Text, + completed: boolean +}; + +export type VisibilityFilter = + 'SHOW_ALL' + | 'SHOW_ACTIVE' + | 'SHOW_COMPLETED' + ; + +export type Todos = Array; + +export type State = { + todos: Todos, + visibilityFilter: VisibilityFilter +}; + +export type Action = + { type: 'ADD_TODO', id: Id, text: Text } + | { type: 'TOGGLE_TODO', id: Id } + | { type: 'SET_VISIBILITY_FILTER', filter: VisibilityFilter } + ; + +export type Store = ReduxStore; + +export type Dispatch = ReduxDispatch; diff --git a/flow-typed/react-redux.js b/flow-typed/react-redux.js new file mode 100644 index 0000000000..99e21ac9ed --- /dev/null +++ b/flow-typed/react-redux.js @@ -0,0 +1,86 @@ +import type { Dispatch, Store } from 'redux' + +declare module 'react-redux' { + + /* + + S = State + A = Action + OP = OwnProps + SP = StateProps + DP = DispatchProps + + */ + + declare type MapStateToProps = (state: S, ownProps: OP) => SP | MapStateToProps; + + declare type MapDispatchToProps = ((dispatch: Dispatch, ownProps: OP) => DP) | DP; + + declare type MergeProps = (stateProps: SP, dispatchProps: DP, ownProps: OP) => P; + + declare type StatelessComponent

    = (props: P) => ?React$Element; + + declare class ConnectedComponent extends React$Component { + static WrappedComponent: Class>; + getWrappedInstance(): React$Component; + static defaultProps: void; + props: OP; + state: void; + } + + declare type ConnectedComponentClass = Class>; + + declare type Connector = { + (component: StatelessComponent

    ): ConnectedComponentClass; + (component: Class>): ConnectedComponentClass; + }; + + declare class Provider extends React$Component, children?: any }, void> { } + + declare type ConnectOptions = { + pure?: boolean, + withRef?: boolean + }; + + declare type Null = null | void; + + declare function connect( + ...rest: Array // <= workaround for https://github.com/facebook/flow/issues/2360 + ): Connector } & OP>>; + + declare function connect( + mapStateToProps: Null, + mapDispatchToProps: Null, + mergeProps: Null, + options: ConnectOptions + ): Connector } & OP>>; + + declare function connect( + mapStateToProps: MapStateToProps, + mapDispatchToProps: Null, + mergeProps: Null, + options?: ConnectOptions + ): Connector } & OP>>; + + declare function connect( + mapStateToProps: Null, + mapDispatchToProps: MapDispatchToProps, + mergeProps: Null, + options?: ConnectOptions + ): Connector>; + + declare function connect( + mapStateToProps: MapStateToProps, + mapDispatchToProps: MapDispatchToProps, + mergeProps: Null, + options?: ConnectOptions + ): Connector>; + + declare function connect( + mapStateToProps: MapStateToProps, + mapDispatchToProps: MapDispatchToProps, + mergeProps: MergeProps, + options?: ConnectOptions + ): Connector; + +} diff --git a/flow-typed/redux.js b/flow-typed/redux.js new file mode 100644 index 0000000000..77ab08f3b1 --- /dev/null +++ b/flow-typed/redux.js @@ -0,0 +1,54 @@ +declare module 'redux' { + + /* + + S = State + A = Action + + */ + + declare type Dispatch }> = (action: A) => A; + + declare type MiddlewareAPI = { + dispatch: Dispatch; + getState(): S; + }; + + declare type Store = { + // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) + dispatch: Dispatch; + getState(): S; + subscribe(listener: () => void): () => void; + replaceReducer(nextReducer: Reducer): void + }; + + declare type Reducer = (state: S, action: A) => S; + + declare type Middleware = + (api: MiddlewareAPI) => + (next: Dispatch) => Dispatch; + + declare type StoreCreator = { + (reducer: Reducer, enhancer?: StoreEnhancer): Store; + (reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; + }; + + declare type StoreEnhancer = (next: StoreCreator) => StoreCreator; + + declare function createStore(reducer: Reducer, enhancer?: StoreEnhancer): Store; + declare function createStore(reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; + + declare function applyMiddleware(...middlewares: Array>): StoreEnhancer; + + declare type ActionCreator = (...args: Array) => A; + declare type ActionCreators = { [key: K]: ActionCreator }; + + declare function bindActionCreators>(actionCreator: C, dispatch: Dispatch): C; + declare function bindActionCreators>(actionCreators: C, dispatch: Dispatch): C; + + // unsafe (you can miss a field and / or assign a wrong reducer to a field) + declare function combineReducers(reducers: {[key: $Keys]: Reducer}): Reducer; + + declare function compose(...fns: Array>): Function; + +}