this.handleSave(todo.id, text)} />
35 | )
36 | } else {
37 | element = (
38 |
39 | completeTodo(todo.id)} />
43 |
46 |
49 | )
50 | }
51 |
52 | return (
53 |
57 | {element}
58 |
59 | )
60 | }
61 | }
62 |
63 | TodoItem.propTypes = {
64 | todo: PropTypes.object.isRequired,
65 | editTodo: PropTypes.func.isRequired,
66 | deleteTodo: PropTypes.func.isRequired,
67 | completeTodo: PropTypes.func.isRequired
68 | }
69 |
70 | export default TodoItem
71 |
--------------------------------------------------------------------------------
/examples/todomvc/components/TodoTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 |
4 | class TodoTextInput extends Component {
5 | constructor(props, context) {
6 | super(props, context)
7 | this.state = {
8 | text: this.props.text || ''
9 | }
10 | }
11 |
12 | handleSubmit(e) {
13 | const text = e.target.value.trim()
14 | if (e.which === 13) {
15 | this.props.onSave(text)
16 | if (this.props.newTodo) {
17 | this.setState({ text: '' })
18 | }
19 | }
20 | }
21 |
22 | handleChange(e) {
23 | this.setState({ text: e.target.value })
24 | }
25 |
26 | handleBlur(e) {
27 | if (!this.props.newTodo) {
28 | this.props.onSave(e.target.value)
29 | }
30 | }
31 |
32 | render() {
33 | return (
34 |
46 | )
47 | }
48 | }
49 |
50 | TodoTextInput.propTypes = {
51 | onSave: PropTypes.func.isRequired,
52 | text: PropTypes.string,
53 | placeholder: PropTypes.string,
54 | editing: PropTypes.bool,
55 | newTodo: PropTypes.bool
56 | }
57 |
58 | export default TodoTextInput
59 |
--------------------------------------------------------------------------------
/examples/todomvc/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO'
2 | export const DELETE_TODO = 'DELETE_TODO'
3 | export const EDIT_TODO = 'EDIT_TODO'
4 | export const COMPLETE_TODO = 'COMPLETE_TODO'
5 | export const COMPLETE_ALL = 'COMPLETE_ALL'
6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'
7 |
--------------------------------------------------------------------------------
/examples/todomvc/constants/TodoFilters.js:
--------------------------------------------------------------------------------
1 | export const SHOW_ALL = 'show_all'
2 | export const SHOW_COMPLETED = 'show_completed'
3 | export const SHOW_ACTIVE = 'show_active'
4 |
--------------------------------------------------------------------------------
/examples/todomvc/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { bindActionCreators } from 'redux'
3 | import { connect } from 'react-redux'
4 | import Header from '../components/Header'
5 | import MainSection from '../components/MainSection'
6 | import * as TodoActions from '../actions'
7 |
8 | class App extends Component {
9 | render() {
10 | const { todos, actions } = this.props
11 | return (
12 |
13 |
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | App.propTypes = {
21 | todos: PropTypes.array.isRequired,
22 | actions: PropTypes.object.isRequired
23 | }
24 |
25 | function mapStateToProps(state) {
26 | return {
27 | todos: state.todos
28 | }
29 | }
30 |
31 | function mapDispatchToProps(dispatch) {
32 | return {
33 | actions: bindActionCreators(TodoActions, dispatch)
34 | }
35 | }
36 |
37 | export default connect(
38 | mapStateToProps,
39 | mapDispatchToProps
40 | )(App)
41 |
--------------------------------------------------------------------------------
/examples/todomvc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux TodoMVC example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/todomvc/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import React from 'react'
3 | import { render } from 'react-dom'
4 | import { Provider } from 'react-redux'
5 | import App from './containers/App'
6 | import configureStore from './store/configureStore'
7 | import 'todomvc-app-css/index.css'
8 |
9 | const store = configureStore()
10 |
11 | render(
12 |
13 |
14 | ,
15 | document.getElementById('root')
16 | )
17 |
--------------------------------------------------------------------------------
/examples/todomvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-todomvc-example",
3 | "version": "0.0.0",
4 | "description": "Redux TodoMVC example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js",
8 | "test:watch": "npm test -- --watch"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/reactjs/redux.git"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/reactjs/redux/issues"
17 | },
18 | "homepage": "http://redux.js.org",
19 | "dependencies": {
20 | "babel-polyfill": "^6.3.14",
21 | "classnames": "^2.1.2",
22 | "react": "^0.14.7",
23 | "react-dom": "^0.14.7",
24 | "react-redux": "^4.2.1",
25 | "redux": "^3.2.1"
26 | },
27 | "devDependencies": {
28 | "babel-core": "^6.3.15",
29 | "babel-loader": "^6.2.0",
30 | "babel-preset-es2015": "^6.3.13",
31 | "babel-preset-react": "^6.3.13",
32 | "babel-preset-react-hmre": "^1.1.1",
33 | "babel-register": "^6.3.13",
34 | "cross-env": "^1.0.7",
35 | "enzyme": "^2.2.0",
36 | "expect": "^1.8.0",
37 | "express": "^4.13.3",
38 | "jsdom": "^5.6.1",
39 | "mocha": "^2.2.5",
40 | "node-libs-browser": "^0.5.2",
41 | "raw-loader": "^0.5.1",
42 | "react-addons-test-utils": "^0.14.7",
43 | "style-loader": "^0.12.3",
44 | "todomvc-app-css": "^2.0.1",
45 | "webpack": "^1.9.11",
46 | "webpack-dev-middleware": "^1.2.0",
47 | "webpack-hot-middleware": "^2.9.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/examples/todomvc/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 |
4 | const rootReducer = combineReducers({
5 | todos
6 | })
7 |
8 | export default rootReducer
9 |
--------------------------------------------------------------------------------
/examples/todomvc/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'
2 |
3 | const initialState = [
4 | {
5 | text: 'Use Redux',
6 | completed: false,
7 | id: 0
8 | }
9 | ]
10 |
11 | export default function todos(state = initialState, action) {
12 | switch (action.type) {
13 | case ADD_TODO:
14 | return [
15 | {
16 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
17 | completed: false,
18 | text: action.text
19 | },
20 | ...state
21 | ]
22 |
23 | case DELETE_TODO:
24 | return state.filter(todo =>
25 | todo.id !== action.id
26 | )
27 |
28 | case EDIT_TODO:
29 | return state.map(todo =>
30 | todo.id === action.id ?
31 | Object.assign({}, todo, { text: action.text }) :
32 | todo
33 | )
34 |
35 | case COMPLETE_TODO:
36 | return state.map(todo =>
37 | todo.id === action.id ?
38 | Object.assign({}, todo, { completed: !todo.completed }) :
39 | todo
40 | )
41 |
42 | case COMPLETE_ALL:
43 | const areAllMarked = state.every(todo => todo.completed)
44 | return state.map(todo => Object.assign({}, todo, {
45 | completed: !areAllMarked
46 | }))
47 |
48 | case CLEAR_COMPLETED:
49 | return state.filter(todo => todo.completed === false)
50 |
51 | default:
52 | return state
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/examples/todomvc/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var webpackDevMiddleware = require('webpack-dev-middleware')
3 | var webpackHotMiddleware = require('webpack-hot-middleware')
4 | var config = require('./webpack.config')
5 |
6 | var app = new (require('express'))()
7 | var port = 3000
8 |
9 | var compiler = webpack(config)
10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
11 | app.use(webpackHotMiddleware(compiler))
12 |
13 | app.get("/", function(req, res) {
14 | res.sendFile(__dirname + '/index.html')
15 | })
16 |
17 | app.listen(port, function(error) {
18 | if (error) {
19 | console.error(error)
20 | } else {
21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/examples/todomvc/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux'
2 | import rootReducer from '../reducers'
3 |
4 | export default function configureStore(preloadedState) {
5 | const store = createStore(rootReducer, preloadedState)
6 |
7 | if (module.hot) {
8 | // Enable Webpack hot module replacement for reducers
9 | module.hot.accept('../reducers', () => {
10 | const nextReducer = require('../reducers').default
11 | store.replaceReducer(nextReducer)
12 | })
13 | }
14 |
15 | return store
16 | }
17 |
--------------------------------------------------------------------------------
/examples/todomvc/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/examples/todomvc/test/actions/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import * as types from '../../constants/ActionTypes'
3 | import * as actions from '../../actions'
4 |
5 | describe('todo actions', () => {
6 | it('addTodo should create ADD_TODO action', () => {
7 | expect(actions.addTodo('Use Redux')).toEqual({
8 | type: types.ADD_TODO,
9 | text: 'Use Redux'
10 | })
11 | })
12 |
13 | it('deleteTodo should create DELETE_TODO action', () => {
14 | expect(actions.deleteTodo(1)).toEqual({
15 | type: types.DELETE_TODO,
16 | id: 1
17 | })
18 | })
19 |
20 | it('editTodo should create EDIT_TODO action', () => {
21 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
22 | type: types.EDIT_TODO,
23 | id: 1,
24 | text: 'Use Redux everywhere'
25 | })
26 | })
27 |
28 | it('completeTodo should create COMPLETE_TODO action', () => {
29 | expect(actions.completeTodo(1)).toEqual({
30 | type: types.COMPLETE_TODO,
31 | id: 1
32 | })
33 | })
34 |
35 | it('completeAll should create COMPLETE_ALL action', () => {
36 | expect(actions.completeAll()).toEqual({
37 | type: types.COMPLETE_ALL
38 | })
39 | })
40 |
41 | it('clearCompleted should create CLEAR_COMPLETED action', () => {
42 | expect(actions.clearCompleted()).toEqual({
43 | type: types.CLEAR_COMPLETED
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/examples/todomvc/test/components/Header.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import Header from '../../components/Header'
5 | import TodoTextInput from '../../components/TodoTextInput'
6 |
7 | function setup() {
8 | const props = {
9 | addTodo: expect.createSpy()
10 | }
11 |
12 | const renderer = TestUtils.createRenderer()
13 | renderer.render()
14 | const output = renderer.getRenderOutput()
15 |
16 | return {
17 | props: props,
18 | output: output,
19 | renderer: renderer
20 | }
21 | }
22 |
23 | describe('components', () => {
24 | describe('Header', () => {
25 | it('should render correctly', () => {
26 | const { output } = setup()
27 |
28 | expect(output.type).toBe('header')
29 | expect(output.props.className).toBe('header')
30 |
31 | const [ h1, input ] = output.props.children
32 |
33 | expect(h1.type).toBe('h1')
34 | expect(h1.props.children).toBe('todos')
35 |
36 | expect(input.type).toBe(TodoTextInput)
37 | expect(input.props.newTodo).toBe(true)
38 | expect(input.props.placeholder).toBe('What needs to be done?')
39 | })
40 |
41 | it('should call addTodo if length of text is greater than 0', () => {
42 | const { output, props } = setup()
43 | const input = output.props.children[1]
44 | input.props.onSave('')
45 | expect(props.addTodo.calls.length).toBe(0)
46 | input.props.onSave('Use Redux')
47 | expect(props.addTodo.calls.length).toBe(1)
48 | })
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/examples/todomvc/test/components/TodoTextInput.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import TodoTextInput from '../../components/TodoTextInput'
5 |
6 | function setup(propOverrides) {
7 | const props = Object.assign({
8 | onSave: expect.createSpy(),
9 | text: 'Use Redux',
10 | placeholder: 'What needs to be done?',
11 | editing: false,
12 | newTodo: false
13 | }, propOverrides)
14 |
15 | const renderer = TestUtils.createRenderer()
16 |
17 | renderer.render(
18 |
19 | )
20 |
21 | const output = renderer.getRenderOutput()
22 |
23 | return {
24 | props: props,
25 | output: output,
26 | renderer: renderer
27 | }
28 | }
29 |
30 | describe('components', () => {
31 | describe('TodoTextInput', () => {
32 | it('should render correctly', () => {
33 | const { output } = setup()
34 | expect(output.props.placeholder).toEqual('What needs to be done?')
35 | expect(output.props.value).toEqual('Use Redux')
36 | expect(output.props.className).toEqual('')
37 | })
38 |
39 | it('should render correctly when editing=true', () => {
40 | const { output } = setup({ editing: true })
41 | expect(output.props.className).toEqual('edit')
42 | })
43 |
44 | it('should render correctly when newTodo=true', () => {
45 | const { output } = setup({ newTodo: true })
46 | expect(output.props.className).toEqual('new-todo')
47 | })
48 |
49 | it('should update value on change', () => {
50 | const { output, renderer } = setup()
51 | output.props.onChange({ target: { value: 'Use Radox' } })
52 | const updated = renderer.getRenderOutput()
53 | expect(updated.props.value).toEqual('Use Radox')
54 | })
55 |
56 | it('should call onSave on return key press', () => {
57 | const { output, props } = setup()
58 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } })
59 | expect(props.onSave).toHaveBeenCalledWith('Use Redux')
60 | })
61 |
62 | it('should reset state on return key press if newTodo', () => {
63 | const { output, renderer } = setup({ newTodo: true })
64 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } })
65 | const updated = renderer.getRenderOutput()
66 | expect(updated.props.value).toEqual('')
67 | })
68 |
69 | it('should call onSave on blur', () => {
70 | const { output, props } = setup()
71 | output.props.onBlur({ target: { value: 'Use Redux' } })
72 | expect(props.onSave).toHaveBeenCalledWith('Use Redux')
73 | })
74 |
75 | it('shouldnt call onSave on blur if newTodo', () => {
76 | const { output, props } = setup({ newTodo: true })
77 | output.props.onBlur({ target: { value: 'Use Redux' } })
78 | expect(props.onSave.calls.length).toBe(0)
79 | })
80 | })
81 | })
82 |
--------------------------------------------------------------------------------
/examples/todomvc/test/setup.js:
--------------------------------------------------------------------------------
1 | import { jsdom } from 'jsdom'
2 |
3 | global.document = jsdom('')
4 | global.window = document.defaultView
5 | global.navigator = global.window.navigator
6 |
--------------------------------------------------------------------------------
/examples/todomvc/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurrenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [
21 | {
22 | test: /\.js$/,
23 | loaders: [ 'babel' ],
24 | exclude: /node_modules/,
25 | include: __dirname
26 | },
27 | {
28 | test: /\.css?$/,
29 | loaders: [ 'style', 'raw' ],
30 | include: __dirname
31 | }
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/actions/index.js:
--------------------------------------------------------------------------------
1 | let nextTodoId = 0
2 | export const addTodo = (text) => {
3 | return {
4 | type: 'ADD_TODO',
5 | id: nextTodoId++,
6 | text
7 | }
8 | }
9 |
10 | export const setVisibilityFilter = (filter) => {
11 | return {
12 | type: 'SET_VISIBILITY_FILTER',
13 | filter
14 | }
15 | }
16 |
17 | export const toggleTodo = (id) => {
18 | return {
19 | type: 'TOGGLE_TODO',
20 | id
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Footer from './Footer'
3 | import AddTodo from '../containers/AddTodo'
4 | import VisibleTodoList from '../containers/VisibleTodoList'
5 | import UndoRedo from '../containers/UndoRedo'
6 |
7 | const App = () => (
8 |
14 | )
15 |
16 | export default App
17 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FilterLink from '../containers/FilterLink'
3 |
4 | const Footer = () => (
5 |
6 | Show:
7 | {" "}
8 |
9 | All
10 |
11 | {", "}
12 |
13 | Active
14 |
15 | {", "}
16 |
17 | Completed
18 |
19 |
20 | )
21 |
22 | export default Footer
23 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/Link.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const Link = ({ active, children, onClick }) => {
4 | if (active) {
5 | return {children}
6 | }
7 |
8 | return (
9 | {
11 | e.preventDefault()
12 | onClick()
13 | }}
14 | >
15 | {children}
16 |
17 | )
18 | }
19 |
20 | Link.propTypes = {
21 | active: PropTypes.bool.isRequired,
22 | children: PropTypes.node.isRequired,
23 | onClick: PropTypes.func.isRequired
24 | }
25 |
26 | export default Link
27 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const Todo = ({ onClick, completed, text }) => (
4 |
10 | {text}
11 |
12 | )
13 |
14 | Todo.propTypes = {
15 | onClick: PropTypes.func.isRequired,
16 | completed: PropTypes.bool.isRequired,
17 | text: PropTypes.string.isRequired
18 | }
19 |
20 | export default Todo
21 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/TodoList.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import Todo from './Todo'
3 |
4 | const TodoList = ({ todos, onTodoClick }) => (
5 |
6 | {todos.map(todo =>
7 | onTodoClick(todo.id)}
11 | />
12 | )}
13 |
14 | )
15 |
16 | TodoList.propTypes = {
17 | todos: PropTypes.arrayOf(PropTypes.shape({
18 | id: PropTypes.number.isRequired,
19 | completed: PropTypes.bool.isRequired,
20 | text: PropTypes.string.isRequired
21 | }).isRequired).isRequired,
22 | onTodoClick: PropTypes.func.isRequired
23 | }
24 |
25 | export default TodoList
26 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/containers/AddTodo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { addTodo } from '../actions'
4 |
5 | let AddTodo = ({ dispatch }) => {
6 | let input
7 |
8 | return (
9 |
10 |
25 |
26 | )
27 | }
28 | AddTodo = connect()(AddTodo)
29 |
30 | export default AddTodo
31 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/containers/FilterLink.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import { setVisibilityFilter } from '../actions'
3 | import Link from '../components/Link'
4 |
5 | const mapStateToProps = (state, ownProps) => {
6 | return {
7 | active: ownProps.filter === state.visibilityFilter
8 | }
9 | }
10 |
11 | const mapDispatchToProps = (dispatch, ownProps) => {
12 | return {
13 | onClick: () => {
14 | dispatch(setVisibilityFilter(ownProps.filter))
15 | }
16 | }
17 | }
18 |
19 | const FilterLink = connect(
20 | mapStateToProps,
21 | mapDispatchToProps
22 | )(Link)
23 |
24 | export default FilterLink
25 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/containers/UndoRedo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ActionCreators as UndoActionCreators } from 'redux-undo'
3 | import { connect } from 'react-redux'
4 |
5 | let UndoRedo = ({ canUndo, canRedo, onUndo, onRedo }) => (
6 |
7 |
10 |
13 |
14 | )
15 |
16 | const mapStateToProps = (state) => {
17 | return {
18 | canUndo: state.todos.past.length > 0,
19 | canRedo: state.todos.future.length > 0
20 | }
21 | }
22 |
23 | const mapDispatchToProps = (dispatch) => {
24 | return {
25 | onUndo: () => dispatch(UndoActionCreators.undo()),
26 | onRedo: () => dispatch(UndoActionCreators.redo())
27 | }
28 | }
29 |
30 | UndoRedo = connect(
31 | mapStateToProps,
32 | mapDispatchToProps
33 | )(UndoRedo)
34 |
35 | export default UndoRedo
36 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/containers/VisibleTodoList.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import { toggleTodo } from '../actions'
3 | import TodoList from '../components/TodoList'
4 |
5 | const getVisibleTodos = (todos, filter) => {
6 | switch (filter) {
7 | case 'SHOW_ALL':
8 | return todos
9 | case 'SHOW_COMPLETED':
10 | return todos.filter(t => t.completed)
11 | case 'SHOW_ACTIVE':
12 | return todos.filter(t => !t.completed)
13 | }
14 | }
15 |
16 | const mapStateToProps = (state) => {
17 | return {
18 | todos: getVisibleTodos(state.todos.present, state.visibilityFilter)
19 | }
20 | }
21 |
22 | const mapDispatchToProps = (dispatch) => {
23 | return {
24 | onTodoClick: (id) => {
25 | dispatch(toggleTodo(id))
26 | }
27 | }
28 | }
29 |
30 | const VisibleTodoList = connect(
31 | mapStateToProps,
32 | mapDispatchToProps
33 | )(TodoList)
34 |
35 | export default VisibleTodoList
36 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux Todos with Undo example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import React from 'react'
3 | import { render } from 'react-dom'
4 | import { createStore } from 'redux'
5 | import { Provider } from 'react-redux'
6 | import App from './components/App'
7 | import todoApp from './reducers'
8 |
9 | const store = createStore(todoApp)
10 |
11 | const rootElement = document.getElementById('root')
12 | render(
13 |
14 |
15 | ,
16 | rootElement
17 | )
18 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-todos-with-undo-example",
3 | "version": "0.0.0",
4 | "description": "Redux todos with undo example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/reactjs/redux.git"
11 | },
12 | "license": "MIT",
13 | "bugs": {
14 | "url": "https://github.com/reactjs/redux/issues"
15 | },
16 | "homepage": "http://redux.js.org",
17 | "dependencies": {
18 | "babel-polyfill": "^6.3.14",
19 | "react": "^0.14.7",
20 | "react-dom": "^0.14.7",
21 | "react-redux": "^4.2.1",
22 | "redux": "^3.2.1",
23 | "redux-undo": "^1.0.0-beta2"
24 | },
25 | "devDependencies": {
26 | "babel-core": "^6.3.15",
27 | "babel-loader": "^6.2.0",
28 | "babel-preset-es2015": "^6.3.13",
29 | "babel-preset-react": "^6.3.13",
30 | "babel-preset-react-hmre": "^1.1.1",
31 | "expect": "^1.6.0",
32 | "express": "^4.13.3",
33 | "node-libs-browser": "^0.5.2",
34 | "webpack": "^1.9.11",
35 | "webpack-dev-middleware": "^1.2.0",
36 | "webpack-hot-middleware": "^2.9.1"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 | import visibilityFilter from './visibilityFilter'
4 |
5 | const todoApp = combineReducers({
6 | todos,
7 | visibilityFilter
8 | })
9 |
10 | export default todoApp
11 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import undoable, { distinctState } from 'redux-undo'
2 |
3 | const todo = (state, action) => {
4 | switch (action.type) {
5 | case 'ADD_TODO':
6 | return {
7 | id: action.id,
8 | text: action.text,
9 | completed: false
10 | }
11 | case 'TOGGLE_TODO':
12 | if (state.id !== action.id) {
13 | return state
14 | }
15 |
16 | return Object.assign({}, state, {
17 | completed: !state.completed
18 | })
19 | default:
20 | return state
21 | }
22 | }
23 |
24 | const todos = (state = [], action) => {
25 | switch (action.type) {
26 | case 'ADD_TODO':
27 | return [
28 | ...state,
29 | todo(undefined, action)
30 | ]
31 | case 'TOGGLE_TODO':
32 | return state.map(t =>
33 | todo(t, action)
34 | )
35 | default:
36 | return state
37 | }
38 | }
39 |
40 | const undoableTodos = undoable(todos, {
41 | filter: distinctState()
42 | })
43 |
44 | export default undoableTodos
45 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/reducers/visibilityFilter.js:
--------------------------------------------------------------------------------
1 | const visibilityFilter = (state = 'SHOW_ALL', action) => {
2 | switch (action.type) {
3 | case 'SET_VISIBILITY_FILTER':
4 | return action.filter
5 | default:
6 | return state
7 | }
8 | }
9 |
10 | export default visibilityFilter
11 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var webpackDevMiddleware = require('webpack-dev-middleware')
3 | var webpackHotMiddleware = require('webpack-hot-middleware')
4 | var config = require('./webpack.config')
5 |
6 | var app = new (require('express'))()
7 | var port = 3000
8 |
9 | var compiler = webpack(config)
10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
11 | app.use(webpackHotMiddleware(compiler))
12 |
13 | app.get("/", function(req, res) {
14 | res.sendFile(__dirname + '/index.html')
15 | })
16 |
17 | app.listen(port, function(error) {
18 | if (error) {
19 | console.error(error)
20 | } else {
21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurrenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [{
21 | test: /\.js$/,
22 | loaders: ['babel'],
23 | exclude: /node_modules/,
24 | include: __dirname
25 | }]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/todos/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/todos/actions/index.js:
--------------------------------------------------------------------------------
1 | let nextTodoId = 0
2 | export const addTodo = (text) => {
3 | return {
4 | type: 'ADD_TODO',
5 | id: nextTodoId++,
6 | text
7 | }
8 | }
9 |
10 | export const setVisibilityFilter = (filter) => {
11 | return {
12 | type: 'SET_VISIBILITY_FILTER',
13 | filter
14 | }
15 | }
16 |
17 | export const toggleTodo = (id) => {
18 | return {
19 | type: 'TOGGLE_TODO',
20 | id
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/todos/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Footer from './Footer'
3 | import AddTodo from '../containers/AddTodo'
4 | import VisibleTodoList from '../containers/VisibleTodoList'
5 |
6 | const App = () => (
7 |
12 | )
13 |
14 | export default App
15 |
--------------------------------------------------------------------------------
/examples/todos/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FilterLink from '../containers/FilterLink'
3 |
4 | const Footer = () => (
5 |
6 | Show:
7 | {" "}
8 |
9 | All
10 |
11 | {", "}
12 |
13 | Active
14 |
15 | {", "}
16 |
17 | Completed
18 |
19 |
20 | )
21 |
22 | export default Footer
23 |
--------------------------------------------------------------------------------
/examples/todos/components/Link.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const Link = ({ active, children, onClick }) => {
4 | if (active) {
5 | return {children}
6 | }
7 |
8 | return (
9 | {
11 | e.preventDefault()
12 | onClick()
13 | }}
14 | >
15 | {children}
16 |
17 | )
18 | }
19 |
20 | Link.propTypes = {
21 | active: PropTypes.bool.isRequired,
22 | children: PropTypes.node.isRequired,
23 | onClick: PropTypes.func.isRequired
24 | }
25 |
26 | export default Link
27 |
--------------------------------------------------------------------------------
/examples/todos/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const Todo = ({ onClick, completed, text }) => (
4 |
10 | {text}
11 |
12 | )
13 |
14 | Todo.propTypes = {
15 | onClick: PropTypes.func.isRequired,
16 | completed: PropTypes.bool.isRequired,
17 | text: PropTypes.string.isRequired
18 | }
19 |
20 | export default Todo
21 |
--------------------------------------------------------------------------------
/examples/todos/components/TodoList.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import Todo from './Todo'
3 |
4 | const TodoList = ({ todos, onTodoClick }) => (
5 |
6 | {todos.map(todo =>
7 | onTodoClick(todo.id)}
11 | />
12 | )}
13 |
14 | )
15 |
16 | TodoList.propTypes = {
17 | todos: PropTypes.arrayOf(PropTypes.shape({
18 | id: PropTypes.number.isRequired,
19 | completed: PropTypes.bool.isRequired,
20 | text: PropTypes.string.isRequired
21 | }).isRequired).isRequired,
22 | onTodoClick: PropTypes.func.isRequired
23 | }
24 |
25 | export default TodoList
26 |
--------------------------------------------------------------------------------
/examples/todos/containers/AddTodo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { addTodo } from '../actions'
4 |
5 | let AddTodo = ({ dispatch }) => {
6 | let input
7 |
8 | return (
9 |
10 |
25 |
26 | )
27 | }
28 | AddTodo = connect()(AddTodo)
29 |
30 | export default AddTodo
31 |
--------------------------------------------------------------------------------
/examples/todos/containers/FilterLink.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import { setVisibilityFilter } from '../actions'
3 | import Link from '../components/Link'
4 |
5 | const mapStateToProps = (state, ownProps) => {
6 | return {
7 | active: ownProps.filter === state.visibilityFilter
8 | }
9 | }
10 |
11 | const mapDispatchToProps = (dispatch, ownProps) => {
12 | return {
13 | onClick: () => {
14 | dispatch(setVisibilityFilter(ownProps.filter))
15 | }
16 | }
17 | }
18 |
19 | const FilterLink = connect(
20 | mapStateToProps,
21 | mapDispatchToProps
22 | )(Link)
23 |
24 | export default FilterLink
25 |
--------------------------------------------------------------------------------
/examples/todos/containers/VisibleTodoList.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import { toggleTodo } from '../actions'
3 | import TodoList from '../components/TodoList'
4 |
5 | const getVisibleTodos = (todos, filter) => {
6 | switch (filter) {
7 | case 'SHOW_ALL':
8 | return todos
9 | case 'SHOW_COMPLETED':
10 | return todos.filter(t => t.completed)
11 | case 'SHOW_ACTIVE':
12 | return todos.filter(t => !t.completed)
13 | }
14 | }
15 |
16 | const mapStateToProps = (state) => {
17 | return {
18 | todos: getVisibleTodos(state.todos, state.visibilityFilter)
19 | }
20 | }
21 |
22 | const mapDispatchToProps = (dispatch) => {
23 | return {
24 | onTodoClick: (id) => {
25 | dispatch(toggleTodo(id))
26 | }
27 | }
28 | }
29 |
30 | const VisibleTodoList = connect(
31 | mapStateToProps,
32 | mapDispatchToProps
33 | )(TodoList)
34 |
35 | export default VisibleTodoList
36 |
--------------------------------------------------------------------------------
/examples/todos/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux Todos example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/todos/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import React from 'react'
3 | import { render } from 'react-dom'
4 | import { Provider } from 'react-redux'
5 | import { createStore } from 'redux'
6 | import todoApp from './reducers'
7 | import App from './components/App'
8 |
9 | let store = createStore(todoApp)
10 |
11 | render(
12 |
13 |
14 | ,
15 | document.getElementById('root')
16 | )
17 |
--------------------------------------------------------------------------------
/examples/todos/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-todos-example",
3 | "version": "0.0.0",
4 | "description": "Redux Todos example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js",
8 | "test:watch": "npm test -- --watch"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/reactjs/redux.git"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/reactjs/redux/issues"
17 | },
18 | "homepage": "http://redux.js.org",
19 | "dependencies": {
20 | "babel-polyfill": "^6.3.14",
21 | "react": "^0.14.7",
22 | "react-dom": "^0.14.7",
23 | "react-redux": "^4.1.2",
24 | "redux": "^3.1.2"
25 | },
26 | "devDependencies": {
27 | "babel-core": "^6.3.15",
28 | "babel-loader": "^6.2.0",
29 | "babel-preset-es2015": "^6.3.13",
30 | "babel-preset-react": "^6.3.13",
31 | "babel-preset-react-hmre": "^1.1.1",
32 | "babel-register": "^6.3.13",
33 | "cross-env": "^1.0.7",
34 | "expect": "^1.8.0",
35 | "express": "^4.13.3",
36 | "jsdom": "^5.6.1",
37 | "mocha": "^2.2.5",
38 | "node-libs-browser": "^0.5.2",
39 | "react-addons-test-utils": "^0.14.7",
40 | "webpack": "^1.9.11",
41 | "webpack-dev-middleware": "^1.2.0",
42 | "webpack-hot-middleware": "^2.9.1"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/examples/todos/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 | import visibilityFilter from './visibilityFilter'
4 |
5 | const todoApp = combineReducers({
6 | todos,
7 | visibilityFilter
8 | })
9 |
10 | export default todoApp
11 |
--------------------------------------------------------------------------------
/examples/todos/reducers/todos.js:
--------------------------------------------------------------------------------
1 | const todo = (state, action) => {
2 | switch (action.type) {
3 | case 'ADD_TODO':
4 | return {
5 | id: action.id,
6 | text: action.text,
7 | completed: false
8 | }
9 | case 'TOGGLE_TODO':
10 | if (state.id !== action.id) {
11 | return state
12 | }
13 |
14 | return Object.assign({}, state, {
15 | completed: !state.completed
16 | })
17 | default:
18 | return state
19 | }
20 | }
21 |
22 | const todos = (state = [], action) => {
23 | switch (action.type) {
24 | case 'ADD_TODO':
25 | return [
26 | ...state,
27 | todo(undefined, action)
28 | ]
29 | case 'TOGGLE_TODO':
30 | return state.map(t =>
31 | todo(t, action)
32 | )
33 | default:
34 | return state
35 | }
36 | }
37 |
38 | export default todos
39 |
--------------------------------------------------------------------------------
/examples/todos/reducers/visibilityFilter.js:
--------------------------------------------------------------------------------
1 | const visibilityFilter = (state = 'SHOW_ALL', action) => {
2 | switch (action.type) {
3 | case 'SET_VISIBILITY_FILTER':
4 | return action.filter
5 | default:
6 | return state
7 | }
8 | }
9 |
10 | export default visibilityFilter
11 |
--------------------------------------------------------------------------------
/examples/todos/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var webpackDevMiddleware = require('webpack-dev-middleware')
3 | var webpackHotMiddleware = require('webpack-hot-middleware')
4 | var config = require('./webpack.config')
5 |
6 | var app = new (require('express'))()
7 | var port = 3000
8 |
9 | var compiler = webpack(config)
10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
11 | app.use(webpackHotMiddleware(compiler))
12 |
13 | app.get("/", function(req, res) {
14 | res.sendFile(__dirname + '/index.html')
15 | })
16 |
17 | app.listen(port, function(error) {
18 | if (error) {
19 | console.error(error)
20 | } else {
21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/examples/todos/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/examples/todos/test/actions/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import * as actions from '../../actions'
3 |
4 | describe('todo actions', () => {
5 | it('addTodo should create ADD_TODO action', () => {
6 | expect(actions.addTodo('Use Redux')).toEqual({
7 | type: 'ADD_TODO',
8 | id: 0,
9 | text: 'Use Redux'
10 | })
11 | })
12 |
13 | it('setVisibilityFilter should create SET_VISIBILITY_FILTER action', () => {
14 | expect(actions.setVisibilityFilter('active')).toEqual({
15 | type: 'SET_VISIBILITY_FILTER',
16 | filter: 'active'
17 | })
18 | })
19 |
20 | it('toggleTodo should create TOGGLE_TODO action', () => {
21 | expect(actions.toggleTodo(1)).toEqual({
22 | type: 'TOGGLE_TODO',
23 | id: 1
24 | })
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/examples/todos/test/reducers/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import todos from '../../reducers/todos'
3 |
4 | describe('todos reducer', () => {
5 | it('should handle initial state', () => {
6 | expect(
7 | todos(undefined, {})
8 | ).toEqual([])
9 | })
10 |
11 | it('should handle ADD_TODO', () => {
12 | expect(
13 | todos([], {
14 | type: 'ADD_TODO',
15 | text: 'Run the tests',
16 | id: 0
17 | })
18 | ).toEqual([
19 | {
20 | text: 'Run the tests',
21 | completed: false,
22 | id: 0
23 | }
24 | ])
25 |
26 | expect(
27 | todos([
28 | {
29 | text: 'Run the tests',
30 | completed: false,
31 | id: 0
32 | }
33 | ], {
34 | type: 'ADD_TODO',
35 | text: 'Use Redux',
36 | id: 1
37 | })
38 | ).toEqual([
39 | {
40 | text: 'Run the tests',
41 | completed: false,
42 | id: 0
43 | }, {
44 | text: 'Use Redux',
45 | completed: false,
46 | id: 1
47 | }
48 | ])
49 |
50 | expect(
51 | todos([
52 | {
53 | text: 'Run the tests',
54 | completed: false,
55 | id: 0
56 | }, {
57 | text: 'Use Redux',
58 | completed: false,
59 | id: 1
60 | }
61 | ], {
62 | type: 'ADD_TODO',
63 | text: 'Fix the tests',
64 | id: 2
65 | })
66 | ).toEqual([
67 | {
68 | text: 'Run the tests',
69 | completed: false,
70 | id: 0
71 | }, {
72 | text: 'Use Redux',
73 | completed: false,
74 | id: 1
75 | }, {
76 | text: 'Fix the tests',
77 | completed: false,
78 | id: 2
79 | }
80 | ])
81 | })
82 |
83 | it('should handle TOGGLE_TODO', () => {
84 | expect(
85 | todos([
86 | {
87 | text: 'Run the tests',
88 | completed: false,
89 | id: 1
90 | }, {
91 | text: 'Use Redux',
92 | completed: false,
93 | id: 0
94 | }
95 | ], {
96 | type: 'TOGGLE_TODO',
97 | id: 1
98 | })
99 | ).toEqual([
100 | {
101 | text: 'Run the tests',
102 | completed: true,
103 | id: 1
104 | }, {
105 | text: 'Use Redux',
106 | completed: false,
107 | id: 0
108 | }
109 | ])
110 | })
111 |
112 | })
113 |
--------------------------------------------------------------------------------
/examples/todos/test/setup.js:
--------------------------------------------------------------------------------
1 | import { jsdom } from 'jsdom'
2 |
3 | global.document = jsdom('')
4 | global.window = document.defaultView
5 | global.navigator = global.window.navigator
6 |
--------------------------------------------------------------------------------
/examples/todos/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurrenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [
21 | {
22 | test: /\.js$/,
23 | loaders: [ 'babel' ],
24 | exclude: /node_modules/,
25 | include: __dirname
26 | }
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/tree-view/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/tree-view/actions/index.js:
--------------------------------------------------------------------------------
1 | export const INCREMENT = 'INCREMENT'
2 | export const CREATE_NODE = 'CREATE_NODE'
3 | export const DELETE_NODE = 'DELETE_NODE'
4 | export const ADD_CHILD = 'ADD_CHILD'
5 | export const REMOVE_CHILD = 'REMOVE_CHILD'
6 |
7 | export function increment(nodeId) {
8 | return {
9 | type: INCREMENT,
10 | nodeId
11 | }
12 | }
13 |
14 | let nextId = 0
15 | export function createNode() {
16 | return {
17 | type: CREATE_NODE,
18 | nodeId: `new_${nextId++}`
19 | }
20 | }
21 |
22 | export function deleteNode(nodeId) {
23 | return {
24 | type: DELETE_NODE,
25 | nodeId
26 | }
27 | }
28 |
29 | export function addChild(nodeId, childId) {
30 | return {
31 | type: ADD_CHILD,
32 | nodeId,
33 | childId
34 | }
35 | }
36 |
37 | export function removeChild(nodeId, childId) {
38 | return {
39 | type: REMOVE_CHILD,
40 | nodeId,
41 | childId
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/examples/tree-view/containers/Node.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Component } from 'react'
3 | import { connect } from 'react-redux'
4 | import * as actions from '../actions'
5 |
6 | export class Node extends Component {
7 | constructor(props) {
8 | super(props)
9 | this.handleIncrementClick = this.handleIncrementClick.bind(this)
10 | this.handleRemoveClick = this.handleRemoveClick.bind(this)
11 | this.handleAddChildClick = this.handleAddChildClick.bind(this)
12 | this.renderChild = this.renderChild.bind(this)
13 | }
14 |
15 | handleIncrementClick() {
16 | const { increment, id } = this.props
17 | increment(id)
18 | }
19 |
20 | handleAddChildClick(e) {
21 | e.preventDefault()
22 |
23 | const { addChild, createNode, id } = this.props
24 | const childId = createNode().nodeId
25 | addChild(id, childId)
26 | }
27 |
28 | handleRemoveClick(e) {
29 | e.preventDefault()
30 |
31 | const { removeChild, deleteNode, parentId, id } = this.props
32 | removeChild(parentId, id)
33 | deleteNode(id)
34 | }
35 |
36 | renderChild(childId) {
37 | const { id } = this.props
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | render() {
46 | const { counter, parentId, childIds } = this.props
47 | return (
48 |
49 | Counter: {counter}
50 | {' '}
51 |
54 | {' '}
55 | {typeof parentId !== 'undefined' ?
56 |
58 | ×
59 | :
60 | null
61 | }
62 |
70 |
71 | )
72 | }
73 | }
74 |
75 | function mapStateToProps(state, ownProps) {
76 | return state[ownProps.id]
77 | }
78 |
79 | const ConnectedNode = connect(mapStateToProps, actions)(Node)
80 | export default ConnectedNode
81 |
--------------------------------------------------------------------------------
/examples/tree-view/generateTree.js:
--------------------------------------------------------------------------------
1 | export default function generateTree() {
2 | let tree = {
3 | 0: {
4 | id: 0,
5 | counter: 0,
6 | childIds: []
7 | }
8 | }
9 |
10 | for (let i = 1; i < 1000; i++) {
11 | let parentId = Math.floor(Math.pow(Math.random(), 2) * i)
12 | tree[i] = {
13 | id: i,
14 | counter: 0,
15 | childIds: []
16 | }
17 | tree[parentId].childIds.push(i)
18 | }
19 |
20 | return tree
21 | }
22 |
--------------------------------------------------------------------------------
/examples/tree-view/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux tree-view example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/tree-view/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import React from 'react'
3 | import { render } from 'react-dom'
4 | import { Provider } from 'react-redux'
5 | import Node from './containers/Node'
6 | import configureStore from './store/configureStore'
7 | import generateTree from './generateTree'
8 |
9 | const tree = generateTree()
10 | const store = configureStore(tree)
11 |
12 | render(
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | )
18 |
--------------------------------------------------------------------------------
/examples/tree-view/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-tree-view-example",
3 | "version": "0.0.0",
4 | "description": "Redux tree-view example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register",
8 | "test:watch": "npm test -- --watch"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/reactjs/redux.git"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/reactjs/redux/issues"
17 | },
18 | "homepage": "http://redux.js.org",
19 | "dependencies": {
20 | "babel-polyfill": "^6.3.14",
21 | "react": "^0.14.7",
22 | "react-dom": "^0.14.7",
23 | "react-redux": "^4.2.1",
24 | "redux": "^3.2.1"
25 | },
26 | "devDependencies": {
27 | "babel-core": "^6.3.15",
28 | "babel-loader": "^6.2.0",
29 | "babel-preset-es2015": "^6.3.13",
30 | "babel-preset-react": "^6.3.13",
31 | "babel-preset-react-hmre": "^1.1.1",
32 | "babel-register": "^6.4.3",
33 | "deep-freeze": "0.0.1",
34 | "cross-env": "^1.0.7",
35 | "enzyme": "^2.0.0",
36 | "expect": "^1.6.0",
37 | "express": "^4.13.3",
38 | "jsdom": "^5.6.1",
39 | "mocha": "^2.2.5",
40 | "node-libs-browser": "^0.5.2",
41 | "react-addons-test-utils": "^0.14.7",
42 | "webpack": "^1.9.11",
43 | "webpack-dev-middleware": "^1.2.0",
44 | "webpack-hot-middleware": "^2.9.1"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/examples/tree-view/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { INCREMENT, ADD_CHILD, REMOVE_CHILD, CREATE_NODE, DELETE_NODE } from '../actions'
2 |
3 | function childIds(state, action) {
4 | switch (action.type) {
5 | case ADD_CHILD:
6 | return [ ...state, action.childId ]
7 | case REMOVE_CHILD:
8 | return state.filter(id => id !== action.childId)
9 | default:
10 | return state
11 | }
12 | }
13 |
14 | function node(state, action) {
15 | switch (action.type) {
16 | case CREATE_NODE:
17 | return {
18 | id: action.nodeId,
19 | counter: 0,
20 | childIds: []
21 | }
22 | case INCREMENT:
23 | return Object.assign({}, state, {
24 | counter: state.counter + 1
25 | })
26 | case ADD_CHILD:
27 | case REMOVE_CHILD:
28 | return Object.assign({}, state, {
29 | childIds: childIds(state.childIds, action)
30 | })
31 | default:
32 | return state
33 | }
34 | }
35 |
36 | function getAllDescendantIds(state, nodeId) {
37 | return state[nodeId].childIds.reduce((acc, childId) => (
38 | [ ...acc, childId, ...getAllDescendantIds(state, childId) ]
39 | ), [])
40 | }
41 |
42 | function deleteMany(state, ids) {
43 | state = Object.assign({}, state)
44 | ids.forEach(id => delete state[id])
45 | return state
46 | }
47 |
48 | export default function (state = {}, action) {
49 | const { nodeId } = action
50 | if (typeof nodeId === 'undefined') {
51 | return state
52 | }
53 |
54 | if (action.type === DELETE_NODE) {
55 | const descendantIds = getAllDescendantIds(state, nodeId)
56 | return deleteMany(state, [ nodeId, ...descendantIds ])
57 | }
58 |
59 | return Object.assign({}, state, {
60 | [nodeId]: node(state[nodeId], action)
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/examples/tree-view/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var webpackDevMiddleware = require('webpack-dev-middleware')
3 | var webpackHotMiddleware = require('webpack-hot-middleware')
4 | var config = require('./webpack.config')
5 |
6 | var app = new (require('express'))()
7 | var port = 3000
8 |
9 | var compiler = webpack(config)
10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
11 | app.use(webpackHotMiddleware(compiler))
12 |
13 | app.get("/", function(req, res) {
14 | res.sendFile(__dirname + '/index.html')
15 | })
16 |
17 | app.listen(port, function(error) {
18 | if (error) {
19 | console.error(error)
20 | } else {
21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/examples/tree-view/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux'
2 | import reducer from '../reducers'
3 |
4 | export default function configureStore(preloadedState) {
5 | const store = createStore(reducer, preloadedState)
6 |
7 | if (module.hot) {
8 | // Enable Webpack hot module replacement for reducers
9 | module.hot.accept('../reducers', () => {
10 | const nextReducer = require('../reducers').default
11 | store.replaceReducer(nextReducer)
12 | })
13 | }
14 |
15 | return store
16 | }
17 |
--------------------------------------------------------------------------------
/examples/tree-view/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/examples/tree-view/test/node.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import { shallow } from 'enzyme'
4 | import ConnectedNode, { Node } from '../containers/Node'
5 |
6 | function setup(id, counter, childIds, parentId) {
7 | const actions = {
8 | increment: expect.createSpy(),
9 | removeChild: expect.createSpy(),
10 | deleteNode: expect.createSpy(),
11 | createNode: expect.createSpy(),
12 | addChild: expect.createSpy()
13 | }
14 |
15 | const eventArgs = {
16 | preventDefault: expect.createSpy()
17 | }
18 |
19 | const component = shallow(
20 |
21 | )
22 |
23 | return {
24 | component: component,
25 | removeLink: component.findWhere(n => n.type() === 'a' && n.contains('×')),
26 | addLink: component.findWhere(n => n.type() === 'a' && n.contains('Add child')),
27 | button: component.find('button'),
28 | childNodes: component.find(ConnectedNode),
29 | actions: actions,
30 | eventArgs: eventArgs
31 | }
32 | }
33 |
34 | describe('Node component', () => {
35 | it('should display counter', () => {
36 | const { component } = setup(1, 23, [])
37 | expect(component.text()).toMatch(/^Counter: 23/)
38 | })
39 |
40 | it('should call increment on button click', () => {
41 | const { button, actions } = setup(1, 23, [])
42 | button.simulate('click')
43 |
44 | expect(actions.increment).toHaveBeenCalledWith(1)
45 | })
46 |
47 | it('should not render remove link', () => {
48 | const { removeLink } = setup(1, 23, [])
49 | expect(removeLink.length).toEqual(0)
50 | })
51 |
52 | it('should call createNode action on Add child click', () => {
53 | const { addLink, actions, eventArgs } = setup(2, 1, [])
54 | actions.createNode.andReturn({ nodeId: 3 })
55 | addLink.simulate('click', eventArgs)
56 |
57 | expect(actions.createNode).toHaveBeenCalled()
58 | })
59 |
60 | it('should call addChild action on Add child click', () => {
61 | const { addLink, actions, eventArgs } = setup(2, 1, [])
62 | actions.createNode.andReturn({ nodeId: 3 })
63 |
64 | addLink.simulate('click', eventArgs)
65 |
66 | expect(actions.addChild).toHaveBeenCalledWith(2, 3)
67 | })
68 |
69 | describe('when given childIds', () => {
70 | it('should render child nodes', () => {
71 | const { childNodes } = setup(1, 23, [ 2, 3 ])
72 | expect(childNodes.length).toEqual(2)
73 | })
74 | })
75 |
76 | describe('when given parentId', () => {
77 | it('should call removeChild action on remove link click', () => {
78 | const { removeLink, actions, eventArgs } = setup(2, 1, [], 1)
79 | removeLink.simulate('click', eventArgs)
80 |
81 | expect(actions.removeChild).toHaveBeenCalledWith(1, 2)
82 | })
83 |
84 | it('should call deleteNode action on remove link click', () => {
85 | const { removeLink, actions, eventArgs } = setup(2, 1, [], 1)
86 | removeLink.simulate('click', eventArgs)
87 |
88 | expect(actions.deleteNode).toHaveBeenCalledWith(2)
89 | })
90 | })
91 | })
92 |
--------------------------------------------------------------------------------
/examples/tree-view/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurrenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [
21 | {
22 | test: /\.js$/,
23 | loaders: [ 'babel' ],
24 | exclude: /node_modules/,
25 | include: __dirname
26 | }
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/universal/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | presets: ["es2015", "react"]
3 | }
--------------------------------------------------------------------------------
/examples/universal/client/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import React from 'react'
3 | import { render } from 'react-dom'
4 | import { Provider } from 'react-redux'
5 | import configureStore from '../common/store/configureStore'
6 | import App from '../common/containers/App'
7 |
8 | const preloadedState = window.__PRELOADED_STATE__
9 | const store = configureStore(preloadedState)
10 | const rootElement = document.getElementById('app')
11 |
12 | render(
13 |
14 |
15 | ,
16 | rootElement
17 | )
18 |
--------------------------------------------------------------------------------
/examples/universal/common/actions/index.js:
--------------------------------------------------------------------------------
1 | export const SET_COUNTER = 'SET_COUNTER'
2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'
3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'
4 |
5 | export function set(value) {
6 | return {
7 | type: SET_COUNTER,
8 | payload: value
9 | }
10 | }
11 |
12 | export function increment() {
13 | return {
14 | type: INCREMENT_COUNTER
15 | }
16 | }
17 |
18 | export function decrement() {
19 | return {
20 | type: DECREMENT_COUNTER
21 | }
22 | }
23 |
24 | export function incrementIfOdd() {
25 | return (dispatch, getState) => {
26 | const { counter } = getState()
27 |
28 | if (counter % 2 === 0) {
29 | return
30 | }
31 |
32 | dispatch(increment())
33 | }
34 | }
35 |
36 | export function incrementAsync(delay = 1000) {
37 | return dispatch => {
38 | setTimeout(() => {
39 | dispatch(increment())
40 | }, delay)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/universal/common/api/counter.js:
--------------------------------------------------------------------------------
1 | function getRandomInt(min, max) {
2 | return Math.floor(Math.random() * (max - min)) + min
3 | }
4 |
5 | export function fetchCounter(callback) {
6 | // Rather than immediately returning, we delay our code with a timeout to simulate asynchronous behavior
7 | setTimeout(() => {
8 | callback(getRandomInt(1, 100))
9 | }, 500)
10 |
11 | // In the case of a real world API call, you'll normally run into a Promise like this:
12 | // API.getUser().then(user => callback(user))
13 | }
14 |
--------------------------------------------------------------------------------
/examples/universal/common/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 |
3 | class Counter extends Component {
4 | render() {
5 | const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props
6 | return (
7 |
8 | Clicked: {counter} times
9 | {' '}
10 |
11 | {' '}
12 |
13 | {' '}
14 |
15 | {' '}
16 |
17 |
18 | )
19 | }
20 | }
21 |
22 | Counter.propTypes = {
23 | increment: PropTypes.func.isRequired,
24 | incrementIfOdd: PropTypes.func.isRequired,
25 | incrementAsync: PropTypes.func.isRequired,
26 | decrement: PropTypes.func.isRequired,
27 | counter: PropTypes.number.isRequired
28 | }
29 |
30 | export default Counter
31 |
--------------------------------------------------------------------------------
/examples/universal/common/containers/App.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux'
2 | import { connect } from 'react-redux'
3 | import Counter from '../components/Counter'
4 | import * as CounterActions from '../actions'
5 |
6 | function mapStateToProps(state) {
7 | return {
8 | counter: state.counter
9 | }
10 | }
11 |
12 | function mapDispatchToProps(dispatch) {
13 | return bindActionCreators(CounterActions, dispatch)
14 | }
15 |
16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter)
17 |
--------------------------------------------------------------------------------
/examples/universal/common/reducers/counter.js:
--------------------------------------------------------------------------------
1 | import { SET_COUNTER, INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions'
2 |
3 | export default function counter(state = 0, action) {
4 | switch (action.type) {
5 | case SET_COUNTER:
6 | return action.payload
7 | case INCREMENT_COUNTER:
8 | return state + 1
9 | case DECREMENT_COUNTER:
10 | return state - 1
11 | default:
12 | return state
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/universal/common/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import counter from './counter'
3 |
4 | const rootReducer = combineReducers({
5 | counter
6 | })
7 |
8 | export default rootReducer
9 |
--------------------------------------------------------------------------------
/examples/universal/common/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import rootReducer from '../reducers'
4 |
5 | export default function configureStore(preloadedState) {
6 | const store = createStore(
7 | rootReducer,
8 | preloadedState,
9 | applyMiddleware(thunk)
10 | )
11 |
12 | if (module.hot) {
13 | // Enable Webpack hot module replacement for reducers
14 | module.hot.accept('../reducers', () => {
15 | const nextRootReducer = require('../reducers').default
16 | store.replaceReducer(nextRootReducer)
17 | })
18 | }
19 |
20 | return store
21 | }
22 |
--------------------------------------------------------------------------------
/examples/universal/index.js:
--------------------------------------------------------------------------------
1 | require('./client')
2 |
--------------------------------------------------------------------------------
/examples/universal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-universal-example",
3 | "version": "0.0.0",
4 | "description": "An example of a universally-rendered Redux application",
5 | "scripts": {
6 | "start": "node server/index.js"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/reactjs/redux.git"
11 | },
12 | "license": "MIT",
13 | "bugs": {
14 | "url": "https://github.com/reactjs/redux/issues"
15 | },
16 | "homepage": "http://redux.js.org",
17 | "dependencies": {
18 | "babel-polyfill": "^6.3.14",
19 | "babel-register": "^6.4.3",
20 | "express": "^4.13.3",
21 | "qs": "^4.0.0",
22 | "react": "^0.14.7",
23 | "react-dom": "^0.14.7",
24 | "react-redux": "^4.2.1",
25 | "redux": "^3.2.1",
26 | "redux-thunk": "^1.0.3",
27 | "serve-static": "^1.10.0"
28 | },
29 | "devDependencies": {
30 | "babel-core": "^6.3.15",
31 | "babel-loader": "^6.2.0",
32 | "babel-preset-es2015": "^6.3.13",
33 | "babel-preset-react": "^6.3.13",
34 | "babel-preset-react-hmre": "^1.1.1",
35 | "babel-runtime": "^6.3.13",
36 | "webpack": "^1.11.0",
37 | "webpack-dev-middleware": "^1.4.0",
38 | "webpack-hot-middleware": "^2.9.1"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/universal/server/index.js:
--------------------------------------------------------------------------------
1 | require('babel-register')
2 | require('./server')
3 |
--------------------------------------------------------------------------------
/examples/universal/server/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, no-use-before-define */
2 |
3 | import path from 'path'
4 | import Express from 'express'
5 | import qs from 'qs'
6 |
7 | import webpack from 'webpack'
8 | import webpackDevMiddleware from 'webpack-dev-middleware'
9 | import webpackHotMiddleware from 'webpack-hot-middleware'
10 | import webpackConfig from '../webpack.config'
11 |
12 | import React from 'react'
13 | import { renderToString } from 'react-dom/server'
14 | import { Provider } from 'react-redux'
15 |
16 | import configureStore from '../common/store/configureStore'
17 | import App from '../common/containers/App'
18 | import { fetchCounter } from '../common/api/counter'
19 |
20 | const app = new Express()
21 | const port = 3000
22 |
23 | // Use this middleware to set up hot module reloading via webpack.
24 | const compiler = webpack(webpackConfig)
25 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath }))
26 | app.use(webpackHotMiddleware(compiler))
27 |
28 | // This is fired every time the server side receives a request
29 | app.use(handleRender)
30 |
31 | function handleRender(req, res) {
32 | // Query our mock API asynchronously
33 | fetchCounter(apiResult => {
34 | // Read the counter from the request, if provided
35 | const params = qs.parse(req.query)
36 | const counter = parseInt(params.counter, 10) || apiResult || 0
37 |
38 | // Compile an initial state
39 | const preloadedState = { counter }
40 |
41 | // Create a new Redux store instance
42 | const store = configureStore(preloadedState)
43 |
44 | // Render the component to a string
45 | const html = renderToString(
46 |
47 |
48 |
49 | )
50 |
51 | // Grab the initial state from our Redux store
52 | const finalState = store.getState()
53 |
54 | // Send the rendered page back to the client
55 | res.send(renderFullPage(html, finalState))
56 | })
57 | }
58 |
59 | function renderFullPage(html, preloadedState) {
60 | return `
61 |
62 |
63 |
64 | Redux Universal Example
65 |
66 |
67 | ${html}
68 |
71 |
72 |
73 |
74 | `
75 | }
76 |
77 | app.listen(port, (error) => {
78 | if (error) {
79 | console.error(error)
80 | } else {
81 | console.info(`==> 🌎 Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`)
82 | }
83 | })
84 |
--------------------------------------------------------------------------------
/examples/universal/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'inline-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './client/index.js'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurrenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [
21 | {
22 | test: /\.js$/,
23 | loader: 'babel',
24 | exclude: /node_modules/,
25 | include: __dirname,
26 | query: {
27 | presets: [ 'react-hmre' ]
28 | }
29 | }
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/logo/README.md:
--------------------------------------------------------------------------------
1 | # The Redux Logo
2 |
3 | This is the only official Redux logo.
4 | Don’t use any other logos to represent Redux.
5 |
6 | It comes in several flavors.
7 |
8 | ## Just the Logo
9 |
10 |
11 |
12 | Download as [PNG](https://raw.githubusercontent.com/reactjs/redux/master/logo/logo.png) or [SVG](https://raw.githubusercontent.com/reactjs/redux/master/logo/logo.svg).
13 |
14 | ## Logo with a Dark Title
15 |
16 |
17 |
18 | Download as [PNG](https://raw.githubusercontent.com/reactjs/redux/master/logo/logo-title-dark.png).
19 |
20 | ## Logo with a Light Title
21 |
22 |
23 |
24 | *(You can’t see the text but it’s there, in white.)*
25 |
26 | Download as [PNG](https://raw.githubusercontent.com/reactjs/redux/master/logo/logo-title-light.png).
27 |
28 | ## Modifications
29 |
30 | Whenever possible, we ask you to use the originals provided on this page.
31 |
32 | If for some reason you must change how the title is rendered and can’t use the prerendered version we provide, we ask that you use the free [Lato Black](http://www.latofonts.com/lato-free-fonts/) font and ensure there is enough space between the logo and the title.
33 |
34 | When in doubt, use the original logos.
35 |
36 | ## Credits
37 |
38 | The Redux logo was designed by [Matthew Johnston](http://thedeskofmatthew.com/).
39 | Thanks to [Jay Phelps](https://github.com/jayphelps) and [the community](https://github.com/reactjs/redux/issues/151) for contributing earlier proposals.
40 |
41 | ## License
42 |
43 | The Redux logo is licensed under CC0, waiving all copyright.
44 | [Read the license.](../LICENSE-logo.md)
45 |
--------------------------------------------------------------------------------
/logo/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chentsulin/redux/43ce29ea13cac12bd66b965dcf4f4a14b0657450/logo/apple-touch-icon.png
--------------------------------------------------------------------------------
/logo/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chentsulin/redux/43ce29ea13cac12bd66b965dcf4f4a14b0657450/logo/favicon.ico
--------------------------------------------------------------------------------
/logo/logo-title-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chentsulin/redux/43ce29ea13cac12bd66b965dcf4f4a14b0657450/logo/logo-title-dark.png
--------------------------------------------------------------------------------
/logo/logo-title-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chentsulin/redux/43ce29ea13cac12bd66b965dcf4f4a14b0657450/logo/logo-title-light.png
--------------------------------------------------------------------------------
/logo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chentsulin/redux/43ce29ea13cac12bd66b965dcf4f4a14b0657450/logo/logo.png
--------------------------------------------------------------------------------
/logo/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/applyMiddleware.js:
--------------------------------------------------------------------------------
1 | import compose from './compose'
2 |
3 | /**
4 | * Creates a store enhancer that applies middleware to the dispatch method
5 | * of the Redux store. This is handy for a variety of tasks, such as expressing
6 | * asynchronous actions in a concise manner, or logging every action payload.
7 | *
8 | * See `redux-thunk` package as an example of the Redux middleware.
9 | *
10 | * Because middleware is potentially asynchronous, this should be the first
11 | * store enhancer in the composition chain.
12 | *
13 | * Note that each middleware will be given the `dispatch` and `getState` functions
14 | * as named arguments.
15 | *
16 | * @param {...Function} middlewares The middleware chain to be applied.
17 | * @returns {Function} A store enhancer applying the middleware.
18 | */
19 | export default function applyMiddleware(...middlewares) {
20 | return (createStore) => (reducer, preloadedState, enhancer) => {
21 | var store = createStore(reducer, preloadedState, enhancer)
22 | var dispatch = store.dispatch
23 | var chain = []
24 |
25 | var middlewareAPI = {
26 | getState: store.getState,
27 | dispatch: (action) => dispatch(action)
28 | }
29 | chain = middlewares.map(middleware => middleware(middlewareAPI))
30 | dispatch = compose(...chain)(store.dispatch)
31 |
32 | return {
33 | ...store,
34 | dispatch
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/bindActionCreators.js:
--------------------------------------------------------------------------------
1 | function bindActionCreator(actionCreator, dispatch) {
2 | return (...args) => dispatch(actionCreator(...args))
3 | }
4 |
5 | /**
6 | * Turns an object whose values are action creators, into an object with the
7 | * same keys, but with every function wrapped into a `dispatch` call so they
8 | * may be invoked directly. This is just a convenience method, as you can call
9 | * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
10 | *
11 | * For convenience, you can also pass a single function as the first argument,
12 | * and get a function in return.
13 | *
14 | * @param {Function|Object} actionCreators An object whose values are action
15 | * creator functions. One handy way to obtain it is to use ES6 `import * as`
16 | * syntax. You may also pass a single function.
17 | *
18 | * @param {Function} dispatch The `dispatch` function available on your Redux
19 | * store.
20 | *
21 | * @returns {Function|Object} The object mimicking the original object, but with
22 | * every action creator wrapped into the `dispatch` call. If you passed a
23 | * function as `actionCreators`, the return value will also be a single
24 | * function.
25 | */
26 | export default function bindActionCreators(actionCreators, dispatch) {
27 | if (typeof actionCreators === 'function') {
28 | return bindActionCreator(actionCreators, dispatch)
29 | }
30 |
31 | if (typeof actionCreators !== 'object' || actionCreators === null) {
32 | throw new Error(
33 | `bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` +
34 | `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
35 | )
36 | }
37 |
38 | var keys = Object.keys(actionCreators)
39 | var boundActionCreators = {}
40 | for (var i = 0; i < keys.length; i++) {
41 | var key = keys[i]
42 | var actionCreator = actionCreators[key]
43 | if (typeof actionCreator === 'function') {
44 | boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
45 | }
46 | }
47 | return boundActionCreators
48 | }
49 |
--------------------------------------------------------------------------------
/src/compose.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Composes single-argument functions from right to left. The rightmost
3 | * function can take multiple arguments as it provides the signature for
4 | * the resulting composite function.
5 | *
6 | * @param {...Function} funcs The functions to compose.
7 | * @returns {Function} A function obtained by composing the argument functions
8 | * from right to left. For example, compose(f, g, h) is identical to doing
9 | * (...args) => f(g(h(...args))).
10 | */
11 |
12 | export default function compose(...funcs) {
13 | if (funcs.length === 0) {
14 | return arg => arg
15 | }
16 |
17 | if (funcs.length === 1) {
18 | return funcs[0]
19 | }
20 |
21 | const last = funcs[funcs.length - 1]
22 | const rest = funcs.slice(0, -1)
23 | return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import createStore from './createStore'
2 | import combineReducers from './combineReducers'
3 | import bindActionCreators from './bindActionCreators'
4 | import applyMiddleware from './applyMiddleware'
5 | import compose from './compose'
6 | import warning from './utils/warning'
7 |
8 | /*
9 | * This is a dummy function to check if the function name has been altered by minification.
10 | * If the function has been minified and NODE_ENV !== 'production', warn the user.
11 | */
12 | function isCrushed() {}
13 |
14 | if (
15 | process.env.NODE_ENV !== 'production' &&
16 | typeof isCrushed.name === 'string' &&
17 | isCrushed.name !== 'isCrushed'
18 | ) {
19 | warning(
20 | 'You are currently using minified code outside of NODE_ENV === \'production\'. ' +
21 | 'This means that you are running a slower development build of Redux. ' +
22 | 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
23 | 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' +
24 | 'to ensure you have the correct code for your production build.'
25 | )
26 | }
27 |
28 | export {
29 | createStore,
30 | combineReducers,
31 | bindActionCreators,
32 | applyMiddleware,
33 | compose
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/warning.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Prints a warning in the console if it exists.
3 | *
4 | * @param {String} message The warning message.
5 | * @returns {void}
6 | */
7 | export default function warning(message) {
8 | /* eslint-disable no-console */
9 | if (typeof console !== 'undefined' && typeof console.error === 'function') {
10 | console.error(message)
11 | }
12 | /* eslint-enable no-console */
13 | try {
14 | // This error was thrown as a convenience so that if you enable
15 | // "break on all exceptions" in your console,
16 | // it would pause the execution at this line.
17 | throw new Error(message)
18 | /* eslint-disable no-empty */
19 | } catch (e) { }
20 | /* eslint-enable no-empty */
21 | }
22 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/compose.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import { compose } from '../src'
3 |
4 | describe('Utils', () => {
5 | describe('compose', () => {
6 | it('composes from right to left', () => {
7 | const double = x => x * 2
8 | const square = x => x * x
9 | expect(compose(square)(5)).toBe(25)
10 | expect(compose(square, double)(5)).toBe(100)
11 | expect(compose(double, square, double)(5)).toBe(200)
12 | })
13 |
14 | it('composes functions from right to left', () => {
15 | const a = next => x => next(x + 'a')
16 | const b = next => x => next(x + 'b')
17 | const c = next => x => next(x + 'c')
18 | const final = x => x
19 |
20 | expect(compose(a, b, c)(final)('')).toBe('abc')
21 | expect(compose(b, c, a)(final)('')).toBe('bca')
22 | expect(compose(c, a, b)(final)('')).toBe('cab')
23 | })
24 |
25 | it('can be seeded with multiple arguments', () => {
26 | const square = x => x * x
27 | const add = (x, y) => x + y
28 | expect(compose(square, add)(1, 2)).toBe(9)
29 | })
30 |
31 | it('returns the first given argument if given no functions', () => {
32 | expect(compose()(1, 2)).toBe(1)
33 | expect(compose()(3)).toBe(3)
34 | expect(compose()()).toBe(undefined)
35 | })
36 |
37 | it('returns the first function if given only one', () => {
38 | const fn = () => {}
39 |
40 | expect(compose(fn)).toBe(fn)
41 | })
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/test/helpers/actionCreators.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_TODO,
3 | DISPATCH_IN_MIDDLE,
4 | GET_STATE_IN_MIDDLE,
5 | SUBSCRIBE_IN_MIDDLE,
6 | UNSUBSCRIBE_IN_MIDDLE,
7 | THROW_ERROR,
8 | UNKNOWN_ACTION
9 | } from './actionTypes'
10 |
11 | export function addTodo(text) {
12 | return { type: ADD_TODO, text }
13 | }
14 |
15 | export function addTodoAsync(text) {
16 | return dispatch => new Promise(resolve => setImmediate(() => {
17 | dispatch(addTodo(text))
18 | resolve()
19 | }))
20 | }
21 |
22 | export function addTodoIfEmpty(text) {
23 | return (dispatch, getState) => {
24 | if (!getState().length) {
25 | dispatch(addTodo(text))
26 | }
27 | }
28 | }
29 |
30 | export function dispatchInMiddle(boundDispatchFn) {
31 | return {
32 | type: DISPATCH_IN_MIDDLE,
33 | boundDispatchFn
34 | }
35 | }
36 |
37 | export function getStateInMiddle(boundGetStateFn) {
38 | return {
39 | type: GET_STATE_IN_MIDDLE,
40 | boundGetStateFn
41 | }
42 | }
43 |
44 | export function subscribeInMiddle(boundSubscribeFn) {
45 | return {
46 | type: SUBSCRIBE_IN_MIDDLE,
47 | boundSubscribeFn
48 | }
49 | }
50 |
51 | export function unsubscribeInMiddle(boundUnsubscribeFn) {
52 | return {
53 | type: UNSUBSCRIBE_IN_MIDDLE,
54 | boundUnsubscribeFn
55 | }
56 | }
57 |
58 | export function throwError() {
59 | return {
60 | type: THROW_ERROR
61 | }
62 | }
63 |
64 | export function unknownAction() {
65 | return {
66 | type: UNKNOWN_ACTION
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/test/helpers/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO'
2 | export const DISPATCH_IN_MIDDLE = 'DISPATCH_IN_MIDDLE'
3 | export const GET_STATE_IN_MIDDLE = 'GET_STATE_IN_MIDDLE'
4 | export const SUBSCRIBE_IN_MIDDLE = 'SUBSCRIBE_IN_MIDDLE'
5 | export const UNSUBSCRIBE_IN_MIDDLE = 'UNSUBSCRIBE_IN_MIDDLE'
6 | export const THROW_ERROR = 'THROW_ERROR'
7 | export const UNKNOWN_ACTION = 'UNKNOWN_ACTION'
8 |
--------------------------------------------------------------------------------
/test/helpers/middleware.js:
--------------------------------------------------------------------------------
1 | export function thunk({ dispatch, getState }) {
2 | return next => action =>
3 | typeof action === 'function' ?
4 | action(dispatch, getState) :
5 | next(action)
6 | }
7 |
--------------------------------------------------------------------------------
/test/helpers/reducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_TODO,
3 | DISPATCH_IN_MIDDLE,
4 | GET_STATE_IN_MIDDLE,
5 | SUBSCRIBE_IN_MIDDLE,
6 | UNSUBSCRIBE_IN_MIDDLE,
7 | THROW_ERROR
8 | } from './actionTypes'
9 |
10 |
11 | function id(state = []) {
12 | return state.reduce((result, item) => (
13 | item.id > result ? item.id : result
14 | ), 0) + 1
15 | }
16 |
17 | export function todos(state = [], action) {
18 | switch (action.type) {
19 | case ADD_TODO:
20 | return [
21 | ...state,
22 | {
23 | id: id(state),
24 | text: action.text
25 | }
26 | ]
27 | default:
28 | return state
29 | }
30 | }
31 |
32 | export function todosReverse(state = [], action) {
33 | switch (action.type) {
34 | case ADD_TODO:
35 | return [
36 | {
37 | id: id(state),
38 | text: action.text
39 | }, ...state
40 | ]
41 | default:
42 | return state
43 | }
44 | }
45 |
46 | export function dispatchInTheMiddleOfReducer(state = [], action) {
47 | switch (action.type) {
48 | case DISPATCH_IN_MIDDLE:
49 | action.boundDispatchFn()
50 | return state
51 | default:
52 | return state
53 | }
54 | }
55 |
56 | export function getStateInTheMiddleOfReducer(state = [], action) {
57 | switch (action.type) {
58 | case GET_STATE_IN_MIDDLE:
59 | action.boundGetStateFn()
60 | return state
61 | default:
62 | return state
63 | }
64 | }
65 |
66 | export function subscribeInTheMiddleOfReducer(state = [], action) {
67 | switch (action.type) {
68 | case SUBSCRIBE_IN_MIDDLE:
69 | action.boundSubscribeFn()
70 | return state
71 | default:
72 | return state
73 | }
74 | }
75 |
76 | export function unsubscribeInTheMiddleOfReducer(state = [], action) {
77 | switch (action.type) {
78 | case UNSUBSCRIBE_IN_MIDDLE:
79 | action.boundUnsubscribeFn()
80 | return state
81 | default:
82 | return state
83 | }
84 | }
85 |
86 | export function errorThrowingReducer(state = [], action) {
87 | switch (action.type) {
88 | case THROW_ERROR:
89 | throw new Error()
90 | default:
91 | return state
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/test/typescript.spec.js:
--------------------------------------------------------------------------------
1 | import * as tt from 'typescript-definition-tester'
2 |
3 |
4 | describe('TypeScript definitions', function () {
5 | this.timeout(0)
6 |
7 | it('should compile against index.d.ts', (done) => {
8 | tt.compileDirectory(
9 | __dirname + '/typescript',
10 | fileName => fileName.match(/\.ts$/),
11 | () => done()
12 | )
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/test/typescript/actionCreators.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ActionCreator, Action, Dispatch,
3 | bindActionCreators, ActionCreatorsMapObject
4 | } from "../../index.d.ts";
5 |
6 |
7 | interface AddTodoAction extends Action {
8 | text: string;
9 | }
10 |
11 | const addTodo: ActionCreator = (text: string) => ({
12 | type: 'ADD_TODO',
13 | text
14 | })
15 |
16 | const addTodoAction: AddTodoAction = addTodo('test');
17 |
18 | type AddTodoThunk = (dispatch: Dispatch) => AddTodoAction;
19 |
20 | const addTodoViaThunk: ActionCreator = (text: string) =>
21 | (dispatch: Dispatch) => ({
22 | type: 'ADD_TODO',
23 | text
24 | })
25 |
26 | declare const dispatch: Dispatch;
27 |
28 | const boundAddTodo = bindActionCreators(addTodo, dispatch);
29 |
30 | const dispatchedAddTodoAction: AddTodoAction = boundAddTodo('test');
31 |
32 |
33 | const boundAddTodoViaThunk = bindActionCreators<
34 | ActionCreator,
35 | ActionCreator
36 | >(addTodoViaThunk, dispatch)
37 |
38 | const dispatchedAddTodoViaThunkAction: AddTodoAction =
39 | boundAddTodoViaThunk('test');
40 |
41 |
42 | const boundActionCreators = bindActionCreators({addTodo}, dispatch);
43 |
44 | const otherDispatchedAddTodoAction: AddTodoAction =
45 | boundActionCreators.addTodo('test');
46 |
47 |
48 | interface M extends ActionCreatorsMapObject {
49 | addTodoViaThunk: ActionCreator
50 | }
51 |
52 | interface N extends ActionCreatorsMapObject {
53 | addTodoViaThunk: ActionCreator
54 | }
55 |
56 | const boundActionCreators2 = bindActionCreators({
57 | addTodoViaThunk
58 | }, dispatch)
59 |
60 | const otherDispatchedAddTodoAction2: AddTodoAction =
61 | boundActionCreators2.addTodoViaThunk('test');
62 |
--------------------------------------------------------------------------------
/test/typescript/actions.ts:
--------------------------------------------------------------------------------
1 | import {Action as ReduxAction} from "../../index.d.ts";
2 |
3 |
4 | namespace FSA {
5 | interface Action extends ReduxAction {
6 | payload: P;
7 | }
8 |
9 | const action: Action = {
10 | type: 'ACTION_TYPE',
11 | payload: 'test',
12 | }
13 |
14 | const payload: string = action.payload;
15 | }
16 |
17 |
18 | namespace FreeShapeAction {
19 | interface Action extends ReduxAction {
20 | [key: string]: any;
21 | }
22 |
23 | const action: Action = {
24 | type: 'ACTION_TYPE',
25 | text: 'test',
26 | }
27 |
28 | const text: string = action['text'];
29 | }
30 |
31 |
32 | namespace StringLiteralTypeAction {
33 | type ActionType = 'A' | 'B' | 'C';
34 |
35 | interface Action extends ReduxAction {
36 | type: ActionType;
37 | }
38 |
39 | const action: Action = {
40 | type: 'A'
41 | }
42 |
43 | const type: ActionType = action.type;
44 | }
45 |
46 |
47 | namespace EnumTypeAction {
48 | enum ActionType {
49 | A, B, C
50 | }
51 |
52 | interface Action extends ReduxAction {
53 | type: ActionType;
54 | }
55 |
56 | const action: Action = {
57 | type: ActionType.A
58 | }
59 |
60 | const type: ActionType = action.type;
61 | }
62 |
--------------------------------------------------------------------------------
/test/typescript/compose.ts:
--------------------------------------------------------------------------------
1 | import {compose} from "../../index.d.ts";
2 |
3 | // copied from DefinitelyTyped/compose-function
4 |
5 | const numberToNumber = (a: number): number => a + 2;
6 | const numberToString = (a: number): string => "foo";
7 | const stringToNumber = (a: string): number => 5;
8 |
9 | const t1: number = compose(numberToNumber, numberToNumber)(5);
10 | const t2: string = compose(numberToString, numberToNumber)(5);
11 | const t3: string = compose(numberToString, stringToNumber)("f");
12 | const t4: (a: string) => number = compose(
13 | (f: (a: string) => number) => ((p: string) => 5),
14 | (f: (a: number) => string) => ((p: string) => 4)
15 | )(numberToString);
16 |
17 |
18 | const t5: number = compose(stringToNumber, numberToString, numberToNumber)(5);
19 | const t6: string = compose(numberToString, stringToNumber, numberToString,
20 | numberToNumber)(5);
21 |
22 | const t7: string = compose(
23 | numberToString, numberToNumber, stringToNumber, numberToString,
24 | stringToNumber)("fo");
25 |
26 |
27 | const multiArgFn = (a: string, b: number, c: boolean): string => 'foo'
28 |
29 | const t8: string = compose(multiArgFn)('bar', 42, true);
30 | const t9: number = compose(stringToNumber, multiArgFn)('bar', 42, true);
31 | const t10: string = compose(numberToString, stringToNumber,
32 | multiArgFn)('bar', 42, true);
33 |
34 | const t11: number = compose(stringToNumber, numberToString, stringToNumber,
35 | multiArgFn)('bar', 42, true);
36 |
--------------------------------------------------------------------------------
/test/typescript/dispatch.ts:
--------------------------------------------------------------------------------
1 | import {Dispatch, Action} from "../../index.d.ts";
2 |
3 |
4 | declare const dispatch: Dispatch;
5 |
6 | const dispatchResult: Action = dispatch({type: 'TYPE'});
7 |
8 | // thunk
9 | declare module "../../index.d.ts" {
10 | export interface Dispatch {
11 | (asyncAction: (dispatch: Dispatch, getState: () => S) => R): R;
12 | }
13 | }
14 |
15 | const dispatchThunkResult: number = dispatch(() => 42);
16 | const dispatchedTimerId: number = dispatch(d => setTimeout(() => d({type: 'TYPE'}), 1000));
17 |
--------------------------------------------------------------------------------
/test/typescript/middleware.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Middleware, MiddlewareAPI,
3 | applyMiddleware, createStore, Dispatch, Reducer, Action
4 | } from "../../index.d.ts";
5 |
6 | declare module "../../index.d.ts" {
7 | export interface Dispatch {
8 | (asyncAction: (dispatch: Dispatch, getState: () => S) => R): R;
9 | }
10 | }
11 |
12 | type Thunk = (dispatch: Dispatch, getState: () => S) => O;
13 |
14 | const thunkMiddleware: Middleware =
15 | ({dispatch, getState}: MiddlewareAPI) =>
16 | (next: Dispatch) =>
17 | (action: A | Thunk): B|Action =>
18 | typeof action === 'function' ?
19 | (>action)(dispatch, getState) :
20 | next(action)
21 |
22 |
23 | const loggerMiddleware: Middleware =
24 | ({getState}: MiddlewareAPI) =>
25 | (next: Dispatch) =>
26 | (action: any): any => {
27 | console.log('will dispatch', action)
28 |
29 | // Call the next dispatch method in the middleware chain.
30 | const returnValue = next(action)
31 |
32 | console.log('state after dispatch', getState())
33 |
34 | // This will likely be the action itself, unless
35 | // a middleware further in chain changed it.
36 | return returnValue
37 | }
38 |
39 |
40 |
41 | type State = {
42 | todos: string[];
43 | }
44 |
45 | const reducer: Reducer = (state: State, action: Action): State => {
46 | return state;
47 | }
48 |
49 | const storeWithThunkMiddleware = createStore(
50 | reducer,
51 | applyMiddleware(thunkMiddleware)
52 | );
53 |
54 | storeWithThunkMiddleware.dispatch(
55 | (dispatch, getState) => {
56 | const todos: string[] = getState().todos;
57 | dispatch({type: 'ADD_TODO'})
58 | }
59 | )
60 |
61 |
62 | const storeWithMultipleMiddleware = createStore(
63 | reducer,
64 | applyMiddleware(loggerMiddleware, thunkMiddleware)
65 | )
66 |
--------------------------------------------------------------------------------
/test/typescript/reducers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Reducer, Action, combineReducers,
3 | ReducersMapObject
4 | } from "../../index.d.ts";
5 |
6 |
7 | type TodosState = string[];
8 |
9 | interface AddTodoAction extends Action {
10 | text: string;
11 | }
12 |
13 |
14 | const todosReducer: Reducer = (state: TodosState,
15 | action: Action): TodosState => {
16 | switch (action.type) {
17 | case 'ADD_TODO':
18 | return [...state, (action).text]
19 | default:
20 | return state
21 | }
22 | }
23 |
24 | const todosState: TodosState = todosReducer([], {
25 | type: 'ADD_TODO',
26 | text: 'test',
27 | });
28 |
29 |
30 | type CounterState = number;
31 |
32 |
33 | const counterReducer: Reducer = (
34 | state: CounterState, action: Action
35 | ): CounterState => {
36 | switch (action.type) {
37 | case 'INCREMENT':
38 | return state + 1
39 | default:
40 | return state
41 | }
42 | }
43 |
44 |
45 | type RootState = {
46 | todos: TodosState;
47 | counter: CounterState;
48 | }
49 |
50 |
51 | const rootReducer: Reducer = combineReducers({
52 | todos: todosReducer,
53 | counter: counterReducer,
54 | })
55 |
56 | const rootState: RootState = rootReducer(undefined, {
57 | type: 'ADD_TODO',
58 | text: 'test',
59 | })
60 |
--------------------------------------------------------------------------------
/test/typescript/store.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Store, createStore, Reducer, Action, StoreEnhancer, GenericStoreEnhancer,
3 | StoreCreator, StoreEnhancerStoreCreator, Unsubscribe
4 | } from "../../index.d.ts";
5 |
6 |
7 | type State = {
8 | todos: string[];
9 | }
10 |
11 | const reducer: Reducer = (state: State, action: Action): State => {
12 | return state;
13 | }
14 |
15 |
16 | /* createStore */
17 |
18 | const store: Store = createStore(reducer);
19 |
20 | const storeWithPreloadedState: Store = createStore(reducer, {
21 | todos: []
22 | });
23 |
24 | const genericEnhancer: GenericStoreEnhancer = (next: StoreEnhancerStoreCreator) => next;
25 | const specificEnhencer: StoreEnhancer = next => next;
26 |
27 | const storeWithGenericEnhancer: Store = createStore(reducer, genericEnhancer);
28 | const storeWithSpecificEnhancer: Store = createStore(reducer, specificEnhencer);
29 |
30 | const storeWithPreloadedStateAndEnhancer: Store = createStore(reducer, {
31 | todos: []
32 | }, genericEnhancer);
33 |
34 |
35 | /* dispatch */
36 |
37 | store.dispatch({
38 | type: 'ADD_TODO',
39 | text: 'test'
40 | })
41 |
42 |
43 | /* getState */
44 |
45 | const state: State = store.getState();
46 |
47 |
48 | /* subscribe / unsubscribe */
49 |
50 | const unsubscribe: Unsubscribe = store.subscribe(() => {
51 | console.log('Current state:', store.getState())
52 | })
53 |
54 | unsubscribe();
55 |
56 |
57 | /* replaceReducer */
58 |
59 | const newReducer: Reducer = (state: State, action: Action): State => {
60 | return state;
61 | }
62 |
63 | store.replaceReducer(newReducer);
64 |
--------------------------------------------------------------------------------
/test/utils/warning.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import warning from '../../src/utils/warning'
3 |
4 | describe('Utils', () => {
5 | describe('warning', () => {
6 | it('calls console.error when available', () => {
7 | const spy = expect.spyOn(console, 'error')
8 | try {
9 | warning('Test')
10 | expect(spy.calls[0].arguments[0]).toBe('Test')
11 | } finally {
12 | spy.restore()
13 | }
14 | })
15 |
16 | it('does not throw when console.error is not available', () => {
17 | const realConsole = global.console
18 | Object.defineProperty(global, 'console', { value: {} })
19 | try {
20 | expect(() => warning('Test')).toNotThrow()
21 | } finally {
22 | Object.defineProperty(global, 'console', { value: realConsole })
23 | }
24 | })
25 |
26 | it('does not throw when console is not available', () => {
27 | const realConsole = global.console
28 | Object.defineProperty(global, 'console', { value: undefined })
29 | try {
30 | expect(() => warning('Test')).toNotThrow()
31 | } finally {
32 | Object.defineProperty(global, 'console', { value: realConsole })
33 | }
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var webpack = require('webpack')
4 |
5 | var env = process.env.NODE_ENV
6 | var config = {
7 | module: {
8 | loaders: [
9 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ }
10 | ]
11 | },
12 | output: {
13 | library: 'Redux',
14 | libraryTarget: 'umd'
15 | },
16 | plugins: [
17 | new webpack.optimize.OccurrenceOrderPlugin(),
18 | new webpack.DefinePlugin({
19 | 'process.env.NODE_ENV': JSON.stringify(env)
20 | })
21 | ]
22 | };
23 |
24 | if (env === 'production') {
25 | config.plugins.push(
26 | new webpack.optimize.UglifyJsPlugin({
27 | compressor: {
28 | pure_getters: true,
29 | unsafe: true,
30 | unsafe_comps: true,
31 | warnings: false
32 | }
33 | })
34 | )
35 | }
36 |
37 | module.exports = config
38 |
--------------------------------------------------------------------------------