diff --git a/examples/async-universal/.babelrc b/examples/async-universal/.babelrc
new file mode 100644
index 0000000000..86c445f545
--- /dev/null
+++ b/examples/async-universal/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["es2015", "react"]
+}
diff --git a/examples/async-universal/client/index.js b/examples/async-universal/client/index.js
new file mode 100644
index 0000000000..b108676ce2
--- /dev/null
+++ b/examples/async-universal/client/index.js
@@ -0,0 +1,21 @@
+import 'babel-polyfill'
+import React from 'react'
+import { render } from 'react-dom'
+import { Provider } from 'react-redux'
+import { Router, browserHistory } from 'react-router'
+
+import configureStore from '../common/store/configureStore'
+import routes from '../common/routes'
+
+const initialState = window.__INITIAL_STATE__
+const store = configureStore(initialState)
+const rootElement = document.getElementById('app')
+
+render(
+
+
+ {routes}
+
+ ,
+ rootElement
+)
diff --git a/examples/async-universal/common/actions/index.js b/examples/async-universal/common/actions/index.js
new file mode 100644
index 0000000000..b3c32cd68d
--- /dev/null
+++ b/examples/async-universal/common/actions/index.js
@@ -0,0 +1,64 @@
+import fetch from 'isomorphic-fetch'
+
+export const REQUEST_POSTS = 'REQUEST_POSTS'
+export const RECEIVE_POSTS = 'RECEIVE_POSTS'
+export const SELECT_REDDIT = 'SELECT_REDDIT'
+export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'
+
+export function selectReddit(reddit) {
+ return {
+ type: SELECT_REDDIT,
+ reddit
+ }
+}
+
+export function invalidateReddit(reddit) {
+ return {
+ type: INVALIDATE_REDDIT,
+ reddit
+ }
+}
+
+function requestPosts(reddit) {
+ return {
+ type: REQUEST_POSTS,
+ reddit
+ }
+}
+
+function receivePosts(reddit, json) {
+ return {
+ type: RECEIVE_POSTS,
+ reddit: reddit,
+ posts: json.data.children.map(child => child.data),
+ receivedAt: Date.now()
+ }
+}
+
+function fetchPosts(reddit) {
+ return dispatch => {
+ dispatch(requestPosts(reddit))
+ return fetch(`https://www.reddit.com/r/${reddit}.json`)
+ .then(response => response.json())
+ .then(json => dispatch(receivePosts(reddit, json)))
+ }
+}
+
+function shouldFetchPosts(state, reddit) {
+ const posts = state.postsByReddit[reddit]
+ if (!posts) {
+ return true
+ }
+ if (posts.isFetching) {
+ return false
+ }
+ return posts.didInvalidate
+}
+
+export function fetchPostsIfNeeded(reddit) {
+ return (dispatch, getState) => {
+ if (shouldFetchPosts(getState(), reddit)) {
+ return dispatch(fetchPosts(reddit))
+ }
+ }
+}
diff --git a/examples/async-universal/common/components/App.js b/examples/async-universal/common/components/App.js
new file mode 100644
index 0000000000..38b41083cc
--- /dev/null
+++ b/examples/async-universal/common/components/App.js
@@ -0,0 +1,16 @@
+import React, { Component } from 'react'
+
+export default class App extends Component {
+ render() {
+ return (
+
+
+
Redux async universal example
+
Code on Github
+
+
+ {this.props.children}
+
+ )
+ }
+}
diff --git a/examples/async-universal/common/components/Picker.js b/examples/async-universal/common/components/Picker.js
new file mode 100644
index 0000000000..2f469af982
--- /dev/null
+++ b/examples/async-universal/common/components/Picker.js
@@ -0,0 +1,29 @@
+import React, { Component, PropTypes } from 'react'
+
+export default class Picker extends Component {
+ render() {
+ const { value, onChange, options } = this.props
+
+ return (
+
+ {(value) ? value : 'Select a subreddit below'}
+
+
+ )
+ }
+}
+
+Picker.propTypes = {
+ options: PropTypes.arrayOf(
+ PropTypes.string.isRequired
+ ).isRequired,
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired
+}
diff --git a/examples/async-universal/common/components/Posts.js b/examples/async-universal/common/components/Posts.js
new file mode 100644
index 0000000000..dd3285dab9
--- /dev/null
+++ b/examples/async-universal/common/components/Posts.js
@@ -0,0 +1,17 @@
+import React, { PropTypes, Component } from 'react'
+
+export default class Posts extends Component {
+ render() {
+ return (
+
+ {this.props.posts.map((post, i) =>
+ - {post.title}
+ )}
+
+ )
+ }
+}
+
+Posts.propTypes = {
+ posts: PropTypes.array.isRequired
+}
diff --git a/examples/async-universal/common/containers/Reddit.js b/examples/async-universal/common/containers/Reddit.js
new file mode 100644
index 0000000000..c678a0b4c2
--- /dev/null
+++ b/examples/async-universal/common/containers/Reddit.js
@@ -0,0 +1,115 @@
+import React, { Component, PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { selectReddit, fetchPostsIfNeeded, invalidateReddit } from '../actions'
+import Picker from '../components/Picker'
+import Posts from '../components/Posts'
+
+class Reddit extends Component {
+
+ constructor(props) {
+ super(props)
+ this.handleChange = this.handleChange.bind(this)
+ this.handleRefreshClick = this.handleRefreshClick.bind(this)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { dispatch, params } = this.props
+
+ if (nextProps.params.id !== params.id) {
+ dispatch(selectReddit(nextProps.params.id))
+ if (nextProps.params.id) {
+ dispatch(fetchPostsIfNeeded(nextProps.params.id))
+ }
+ }
+
+ }
+
+ handleChange(nextReddit) {
+ this.context.router.push(`/${nextReddit}`)
+ }
+
+ handleRefreshClick(e) {
+ e.preventDefault()
+
+ const { dispatch, selectedReddit } = this.props
+ dispatch(invalidateReddit(selectedReddit))
+ dispatch(fetchPostsIfNeeded(selectedReddit))
+ }
+
+ render() {
+ const { selectedReddit, posts, isFetching, lastUpdated } = this.props
+ const isEmpty = posts.length === 0
+ return (
+
+
+
+ {lastUpdated &&
+
+ Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
+ {' '}
+
+ }
+ {!isFetching && selectedReddit &&
+
+ Refresh
+
+ }
+
+ {isEmpty
+ ? (isFetching ?
Loading...
:
Empty.
)
+ :
+ }
+
+ )
+ }
+}
+
+Reddit.fetchData = (dispatch, params) => {
+ const subreddit = params.id
+ if (subreddit) {
+ return Promise.all([
+ dispatch(selectReddit(subreddit)),
+ dispatch(fetchPostsIfNeeded(subreddit))
+ ])
+ } else {
+ return Promise.resolve()
+ }
+}
+
+Reddit.contextTypes = {
+ router: PropTypes.object
+}
+
+Reddit.propTypes = {
+ selectedReddit: PropTypes.string.isRequired,
+ posts: PropTypes.array.isRequired,
+ isFetching: PropTypes.bool.isRequired,
+ lastUpdated: PropTypes.number,
+ dispatch: PropTypes.func.isRequired
+}
+
+function mapStateToProps(state) {
+ const { selectedReddit, postsByReddit } = state
+ const {
+ isFetching,
+ lastUpdated,
+ items: posts
+ } = postsByReddit[selectedReddit] || {
+ isFetching: false,
+ items: []
+ }
+
+ return {
+ selectedReddit,
+ posts,
+ isFetching,
+ lastUpdated
+ }
+}
+
+export default connect(mapStateToProps)(Reddit)
diff --git a/examples/async-universal/common/reducers/index.js b/examples/async-universal/common/reducers/index.js
new file mode 100644
index 0000000000..d6836ee0ff
--- /dev/null
+++ b/examples/async-universal/common/reducers/index.js
@@ -0,0 +1,61 @@
+import { combineReducers } from 'redux'
+import {
+ SELECT_REDDIT, INVALIDATE_REDDIT,
+ REQUEST_POSTS, RECEIVE_POSTS
+} from '../actions'
+
+function selectedReddit(state = '', action) {
+ switch (action.type) {
+ case SELECT_REDDIT:
+ return action.reddit || ''
+ default:
+ return state
+ }
+}
+
+function posts(state = {
+ isFetching: false,
+ didInvalidate: false,
+ items: []
+}, action) {
+ switch (action.type) {
+ case INVALIDATE_REDDIT:
+ return Object.assign({}, state, {
+ didInvalidate: true
+ })
+ case REQUEST_POSTS:
+ return Object.assign({}, state, {
+ isFetching: true,
+ didInvalidate: false
+ })
+ case RECEIVE_POSTS:
+ return Object.assign({}, state, {
+ isFetching: false,
+ didInvalidate: false,
+ items: action.posts,
+ lastUpdated: action.receivedAt
+ })
+ default:
+ return state
+ }
+}
+
+function postsByReddit(state = { }, action) {
+ switch (action.type) {
+ case INVALIDATE_REDDIT:
+ case RECEIVE_POSTS:
+ case REQUEST_POSTS:
+ return Object.assign({}, state, {
+ [action.reddit]: posts(state[action.reddit], action)
+ })
+ default:
+ return state
+ }
+}
+
+const rootReducer = combineReducers({
+ postsByReddit,
+ selectedReddit
+})
+
+export default rootReducer
diff --git a/examples/async-universal/common/routes.js b/examples/async-universal/common/routes.js
new file mode 100644
index 0000000000..0d8bb3717c
--- /dev/null
+++ b/examples/async-universal/common/routes.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import Route from 'react-router/lib/Route'
+import IndexRoute from 'react-router/lib/IndexRoute'
+
+import App from './components/App'
+import Reddit from './containers/Reddit'
+
+export default (
+
+
+
+
+)
diff --git a/examples/async-universal/common/store/configureStore.js b/examples/async-universal/common/store/configureStore.js
new file mode 100644
index 0000000000..465d94919a
--- /dev/null
+++ b/examples/async-universal/common/store/configureStore.js
@@ -0,0 +1,22 @@
+import { createStore, applyMiddleware } from 'redux'
+import thunkMiddleware from 'redux-thunk'
+import createLogger from 'redux-logger'
+import rootReducer from '../reducers'
+
+export default function configureStore(initialState) {
+ const store = createStore(
+ rootReducer,
+ initialState,
+ applyMiddleware(thunkMiddleware, createLogger())
+ )
+
+ if (module.hot) {
+ // Enable Webpack hot module replacement for reducers
+ module.hot.accept('../reducers', () => {
+ const nextRootReducer = require('../reducers').default
+ store.replaceReducer(nextRootReducer)
+ })
+ }
+
+ return store
+}
diff --git a/examples/async-universal/package.json b/examples/async-universal/package.json
new file mode 100644
index 0000000000..123579a793
--- /dev/null
+++ b/examples/async-universal/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "redux-async-universal-example",
+ "version": "0.0.0",
+ "description": "Redux async universal example",
+ "scripts": {
+ "start": "node server/index.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/reactjs/redux.git"
+ },
+ "keywords": [
+ "react",
+ "reactjs",
+ "hot",
+ "reload",
+ "hmr",
+ "live",
+ "edit",
+ "webpack",
+ "flux"
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactjs/redux/issues"
+ },
+ "homepage": "http://redux.js.org",
+ "dependencies": {
+ "babel-polyfill": "^6.3.14",
+ "express": "^4.13.4",
+ "isomorphic-fetch": "^2.1.1",
+ "react": "^0.14.7",
+ "react-dom": "^0.14.7",
+ "react-redux": "^4.2.1",
+ "react-router": "^2.0.0",
+ "redux": "^3.2.1",
+ "redux-logger": "^2.4.0",
+ "redux-thunk": "^1.0.3"
+ },
+ "devDependencies": {
+ "babel-core": "^6.3.15",
+ "babel-loader": "^6.2.0",
+ "babel-preset-es2015": "^6.3.13",
+ "babel-preset-react": "^6.3.13",
+ "expect": "^1.6.0",
+ "node-libs-browser": "^0.5.2",
+ "react-transform-hmr": "^1.0.4",
+ "webpack": "^1.9.11",
+ "webpack-dev-middleware": "^1.2.0",
+ "webpack-hot-middleware": "^2.2.0"
+ }
+}
diff --git a/examples/async-universal/server/index.js b/examples/async-universal/server/index.js
new file mode 100644
index 0000000000..04171d26bf
--- /dev/null
+++ b/examples/async-universal/server/index.js
@@ -0,0 +1,2 @@
+require('babel-register')
+require('./server')
diff --git a/examples/async-universal/server/server.js b/examples/async-universal/server/server.js
new file mode 100644
index 0000000000..7d00807214
--- /dev/null
+++ b/examples/async-universal/server/server.js
@@ -0,0 +1,80 @@
+var webpack = require('webpack')
+var webpackDevMiddleware = require('webpack-dev-middleware')
+var webpackHotMiddleware = require('webpack-hot-middleware')
+var config = require('../webpack.config')
+var React = require('react')
+var renderToString = require('react-dom/server').renderToString
+var Provider = require('react-redux').Provider
+var match = require('react-router/lib/match')
+var RouterContext = require('react-router/lib/RouterContext')
+
+var configureStore = require('../common/store/configureStore').default
+var routes = require('../common/routes').default
+
+var app = new (require('express'))()
+var port = 3000
+
+var compiler = webpack(config)
+app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
+app.use(webpackHotMiddleware(compiler))
+
+app.use(handleRender)
+
+function handleRender(req, res) {
+ match({ routes, location: req.url }, function(error, redirectLocation, renderProps) {
+ if (error) {
+ res.status(500).send(error.message)
+ } else if (redirectLocation) {
+ res.redirect(302, redirectLocation.pathname + redirectLocation.search)
+ } else if (renderProps) {
+ // Create a new Redux store instance
+ var store = configureStore()
+
+ // Grab static fetchData
+ var fetchData = renderProps.components[ renderProps.components.length - 1 ].fetchData
+
+ // Query our API asynchronously
+ fetchData(store.dispatch, renderProps.params).then(() => {
+
+ const html = renderToString(
+
+
+
+ )
+
+ var finalState = store.getState()
+
+ res.status(200).send(renderFullPage(html, finalState))
+ })
+
+ } else {
+ res.status(404).send('Not found')
+ }
+ })
+}
+
+function renderFullPage(html, initialState) {
+ return `
+
+
+
+ Redux Async Universal Example
+
+
+ ${html}
+
+
+
+
+ `
+}
+
+app.listen(port, function(error) {
+ if (error) {
+ console.error(error)
+ } else {
+ console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
+ }
+})
diff --git a/examples/async-universal/webpack.config.js b/examples/async-universal/webpack.config.js
new file mode 100644
index 0000000000..5aaa90d519
--- /dev/null
+++ b/examples/async-universal/webpack.config.js
@@ -0,0 +1,29 @@
+var path = require('path')
+var webpack = require('webpack')
+
+module.exports = {
+ devtool: 'cheap-module-eval-source-map',
+ entry: [
+ 'webpack-hot-middleware/client',
+ './client/index.js'
+ ],
+ output: {
+ path: path.join(__dirname, 'dist'),
+ filename: 'bundle.js',
+ publicPath: '/static/'
+ },
+ plugins: [
+ new webpack.optimize.OccurenceOrderPlugin(),
+ new webpack.HotModuleReplacementPlugin()
+ ],
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ loaders: ['babel'],
+ exclude: /node_modules/,
+ include: __dirname
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/examples/async-with-routing/.babelrc b/examples/async-with-routing/.babelrc
new file mode 100644
index 0000000000..d0962f5695
--- /dev/null
+++ b/examples/async-with-routing/.babelrc
@@ -0,0 +1,8 @@
+{
+ "presets": ["es2015", "react"],
+ "env": {
+ "development": {
+ "presets": ["react-hmre"]
+ }
+ }
+}
diff --git a/examples/async-with-routing/actions/index.js b/examples/async-with-routing/actions/index.js
new file mode 100644
index 0000000000..b3c32cd68d
--- /dev/null
+++ b/examples/async-with-routing/actions/index.js
@@ -0,0 +1,64 @@
+import fetch from 'isomorphic-fetch'
+
+export const REQUEST_POSTS = 'REQUEST_POSTS'
+export const RECEIVE_POSTS = 'RECEIVE_POSTS'
+export const SELECT_REDDIT = 'SELECT_REDDIT'
+export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'
+
+export function selectReddit(reddit) {
+ return {
+ type: SELECT_REDDIT,
+ reddit
+ }
+}
+
+export function invalidateReddit(reddit) {
+ return {
+ type: INVALIDATE_REDDIT,
+ reddit
+ }
+}
+
+function requestPosts(reddit) {
+ return {
+ type: REQUEST_POSTS,
+ reddit
+ }
+}
+
+function receivePosts(reddit, json) {
+ return {
+ type: RECEIVE_POSTS,
+ reddit: reddit,
+ posts: json.data.children.map(child => child.data),
+ receivedAt: Date.now()
+ }
+}
+
+function fetchPosts(reddit) {
+ return dispatch => {
+ dispatch(requestPosts(reddit))
+ return fetch(`https://www.reddit.com/r/${reddit}.json`)
+ .then(response => response.json())
+ .then(json => dispatch(receivePosts(reddit, json)))
+ }
+}
+
+function shouldFetchPosts(state, reddit) {
+ const posts = state.postsByReddit[reddit]
+ if (!posts) {
+ return true
+ }
+ if (posts.isFetching) {
+ return false
+ }
+ return posts.didInvalidate
+}
+
+export function fetchPostsIfNeeded(reddit) {
+ return (dispatch, getState) => {
+ if (shouldFetchPosts(getState(), reddit)) {
+ return dispatch(fetchPosts(reddit))
+ }
+ }
+}
diff --git a/examples/async-with-routing/components/App.js b/examples/async-with-routing/components/App.js
new file mode 100644
index 0000000000..a054acff6c
--- /dev/null
+++ b/examples/async-with-routing/components/App.js
@@ -0,0 +1,16 @@
+import React, { Component } from 'react'
+
+export default class App extends Component {
+ render() {
+ return (
+
+
+
Redux async with router example
+
Code on Github
+
+
+ {this.props.children}
+
+ )
+ }
+}
diff --git a/examples/async-with-routing/components/Picker.js b/examples/async-with-routing/components/Picker.js
new file mode 100644
index 0000000000..2f469af982
--- /dev/null
+++ b/examples/async-with-routing/components/Picker.js
@@ -0,0 +1,29 @@
+import React, { Component, PropTypes } from 'react'
+
+export default class Picker extends Component {
+ render() {
+ const { value, onChange, options } = this.props
+
+ return (
+
+ {(value) ? value : 'Select a subreddit below'}
+
+
+ )
+ }
+}
+
+Picker.propTypes = {
+ options: PropTypes.arrayOf(
+ PropTypes.string.isRequired
+ ).isRequired,
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired
+}
diff --git a/examples/async-with-routing/components/Posts.js b/examples/async-with-routing/components/Posts.js
new file mode 100644
index 0000000000..dd3285dab9
--- /dev/null
+++ b/examples/async-with-routing/components/Posts.js
@@ -0,0 +1,17 @@
+import React, { PropTypes, Component } from 'react'
+
+export default class Posts extends Component {
+ render() {
+ return (
+
+ {this.props.posts.map((post, i) =>
+ - {post.title}
+ )}
+
+ )
+ }
+}
+
+Posts.propTypes = {
+ posts: PropTypes.array.isRequired
+}
diff --git a/examples/async-with-routing/containers/Reddit.js b/examples/async-with-routing/containers/Reddit.js
new file mode 100644
index 0000000000..8cff005988
--- /dev/null
+++ b/examples/async-with-routing/containers/Reddit.js
@@ -0,0 +1,112 @@
+import React, { Component, PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { selectReddit, fetchPostsIfNeeded, invalidateReddit } from '../actions'
+import Picker from '../components/Picker'
+import Posts from '../components/Posts'
+
+class Reddit extends Component {
+
+ constructor(props) {
+ super(props)
+ this.handleChange = this.handleChange.bind(this)
+ this.handleRefreshClick = this.handleRefreshClick.bind(this)
+ }
+
+ componentDidMount() {
+ const { dispatch, params } = this.props
+ if (params.id) {
+ dispatch(selectReddit(params.id))
+ dispatch(fetchPostsIfNeeded(params.id))
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { dispatch, params } = this.props
+
+ if (nextProps.params.id !== params.id) {
+ dispatch(selectReddit(nextProps.params.id))
+ if (nextProps.params.id) {
+ dispatch(fetchPostsIfNeeded(nextProps.params.id))
+ }
+ }
+
+ }
+
+ handleChange(nextReddit) {
+ this.context.router.push(`/${nextReddit}`)
+ }
+
+ handleRefreshClick(e) {
+ e.preventDefault()
+
+ const { dispatch, selectedReddit } = this.props
+ dispatch(invalidateReddit(selectedReddit))
+ dispatch(fetchPostsIfNeeded(selectedReddit))
+ }
+
+ render() {
+ const { selectedReddit, posts, isFetching, lastUpdated } = this.props
+ const isEmpty = posts.length === 0
+
+ return (
+
+
+
+ {lastUpdated &&
+
+ Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
+ {' '}
+
+ }
+ {!isFetching && selectedReddit &&
+
+ Refresh
+
+ }
+
+ {isEmpty
+ ? (isFetching ?
Loading...
:
Empty.
)
+ :
+ }
+
+ )
+ }
+}
+
+Reddit.contextTypes = {
+ router: PropTypes.object
+}
+
+Reddit.propTypes = {
+ selectedReddit: PropTypes.string.isRequired,
+ posts: PropTypes.array.isRequired,
+ isFetching: PropTypes.bool.isRequired,
+ lastUpdated: PropTypes.number,
+ dispatch: PropTypes.func.isRequired
+}
+
+function mapStateToProps(state) {
+ const { selectedReddit, postsByReddit } = state
+ const {
+ isFetching,
+ lastUpdated,
+ items: posts
+ } = postsByReddit[selectedReddit] || {
+ isFetching: false,
+ items: []
+ }
+
+ return {
+ selectedReddit,
+ posts,
+ isFetching,
+ lastUpdated
+ }
+}
+
+export default connect(mapStateToProps)(Reddit)
diff --git a/examples/async-with-routing/index.html b/examples/async-with-routing/index.html
new file mode 100644
index 0000000000..b9b31b6ff1
--- /dev/null
+++ b/examples/async-with-routing/index.html
@@ -0,0 +1,11 @@
+
+
+
+ Redux async with routing example
+
+
+
+
+
+
+
diff --git a/examples/async-with-routing/index.js b/examples/async-with-routing/index.js
new file mode 100644
index 0000000000..2c8735b9b7
--- /dev/null
+++ b/examples/async-with-routing/index.js
@@ -0,0 +1,19 @@
+import 'babel-polyfill'
+import React from 'react'
+import { render } from 'react-dom'
+import Router from 'react-router/lib/Router'
+import browserHistory from 'react-router/lib/browserHistory'
+import { Provider } from 'react-redux'
+import routes from './routes'
+import configureStore from './store/configureStore'
+
+const store = configureStore()
+
+render(
+
+
+ {routes}
+
+ ,
+ document.getElementById('root')
+)
diff --git a/examples/async-with-routing/package.json b/examples/async-with-routing/package.json
new file mode 100644
index 0000000000..805503889b
--- /dev/null
+++ b/examples/async-with-routing/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "redux-async-with-routing-example",
+ "version": "0.0.0",
+ "description": "Redux async with routing example",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/reactjs/redux.git"
+ },
+ "keywords": [
+ "react",
+ "reactjs",
+ "hot",
+ "reload",
+ "hmr",
+ "live",
+ "edit",
+ "webpack",
+ "flux"
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactjs/redux/issues"
+ },
+ "homepage": "http://redux.js.org",
+ "dependencies": {
+ "babel-polyfill": "^6.3.14",
+ "isomorphic-fetch": "^2.1.1",
+ "react": "^0.14.7",
+ "react-dom": "^0.14.7",
+ "react-redux": "^4.2.1",
+ "react-router": "^2.0.0",
+ "redux": "^3.2.1",
+ "redux-logger": "^2.4.0",
+ "redux-thunk": "^1.0.3"
+ },
+ "devDependencies": {
+ "babel-core": "^6.3.15",
+ "babel-loader": "^6.2.0",
+ "babel-preset-es2015": "^6.3.13",
+ "babel-preset-react": "^6.3.13",
+ "babel-preset-react-hmre": "^1.0.1",
+ "expect": "^1.6.0",
+ "express": "^4.13.3",
+ "node-libs-browser": "^0.5.2",
+ "webpack": "^1.9.11",
+ "webpack-dev-middleware": "^1.2.0",
+ "webpack-hot-middleware": "^2.2.0"
+ }
+}
diff --git a/examples/async-with-routing/reducers/index.js b/examples/async-with-routing/reducers/index.js
new file mode 100644
index 0000000000..d6836ee0ff
--- /dev/null
+++ b/examples/async-with-routing/reducers/index.js
@@ -0,0 +1,61 @@
+import { combineReducers } from 'redux'
+import {
+ SELECT_REDDIT, INVALIDATE_REDDIT,
+ REQUEST_POSTS, RECEIVE_POSTS
+} from '../actions'
+
+function selectedReddit(state = '', action) {
+ switch (action.type) {
+ case SELECT_REDDIT:
+ return action.reddit || ''
+ default:
+ return state
+ }
+}
+
+function posts(state = {
+ isFetching: false,
+ didInvalidate: false,
+ items: []
+}, action) {
+ switch (action.type) {
+ case INVALIDATE_REDDIT:
+ return Object.assign({}, state, {
+ didInvalidate: true
+ })
+ case REQUEST_POSTS:
+ return Object.assign({}, state, {
+ isFetching: true,
+ didInvalidate: false
+ })
+ case RECEIVE_POSTS:
+ return Object.assign({}, state, {
+ isFetching: false,
+ didInvalidate: false,
+ items: action.posts,
+ lastUpdated: action.receivedAt
+ })
+ default:
+ return state
+ }
+}
+
+function postsByReddit(state = { }, action) {
+ switch (action.type) {
+ case INVALIDATE_REDDIT:
+ case RECEIVE_POSTS:
+ case REQUEST_POSTS:
+ return Object.assign({}, state, {
+ [action.reddit]: posts(state[action.reddit], action)
+ })
+ default:
+ return state
+ }
+}
+
+const rootReducer = combineReducers({
+ postsByReddit,
+ selectedReddit
+})
+
+export default rootReducer
diff --git a/examples/async-with-routing/routes.js b/examples/async-with-routing/routes.js
new file mode 100644
index 0000000000..2d1dd4abe8
--- /dev/null
+++ b/examples/async-with-routing/routes.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import Route from 'react-router/lib/Route'
+import IndexRoute from 'react-router/lib/IndexRoute'
+
+import App from './components/App'
+import Reddit from './containers/Reddit'
+
+export default(
+
+
+
+
+)
diff --git a/examples/async-with-routing/server.js b/examples/async-with-routing/server.js
new file mode 100644
index 0000000000..9aeb674633
--- /dev/null
+++ b/examples/async-with-routing/server.js
@@ -0,0 +1,23 @@
+var webpack = require('webpack')
+var webpackDevMiddleware = require('webpack-dev-middleware')
+var webpackHotMiddleware = require('webpack-hot-middleware')
+var config = require('./webpack.config')
+
+var app = new (require('express'))()
+var port = 3000
+
+var compiler = webpack(config)
+app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
+app.use(webpackHotMiddleware(compiler))
+
+app.get("*", function(req, res) {
+ res.sendFile(__dirname + '/index.html')
+})
+
+app.listen(port, function(error) {
+ if (error) {
+ console.error(error)
+ } else {
+ console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
+ }
+})
diff --git a/examples/async-with-routing/store/configureStore.js b/examples/async-with-routing/store/configureStore.js
new file mode 100644
index 0000000000..465d94919a
--- /dev/null
+++ b/examples/async-with-routing/store/configureStore.js
@@ -0,0 +1,22 @@
+import { createStore, applyMiddleware } from 'redux'
+import thunkMiddleware from 'redux-thunk'
+import createLogger from 'redux-logger'
+import rootReducer from '../reducers'
+
+export default function configureStore(initialState) {
+ const store = createStore(
+ rootReducer,
+ initialState,
+ applyMiddleware(thunkMiddleware, createLogger())
+ )
+
+ if (module.hot) {
+ // Enable Webpack hot module replacement for reducers
+ module.hot.accept('../reducers', () => {
+ const nextRootReducer = require('../reducers').default
+ store.replaceReducer(nextRootReducer)
+ })
+ }
+
+ return store
+}
diff --git a/examples/async-with-routing/webpack.config.js b/examples/async-with-routing/webpack.config.js
new file mode 100644
index 0000000000..35062a810d
--- /dev/null
+++ b/examples/async-with-routing/webpack.config.js
@@ -0,0 +1,29 @@
+var path = require('path')
+var webpack = require('webpack')
+
+module.exports = {
+ devtool: 'cheap-module-eval-source-map',
+ entry: [
+ 'webpack-hot-middleware/client',
+ './index'
+ ],
+ output: {
+ path: path.join(__dirname, 'dist'),
+ filename: 'bundle.js',
+ publicPath: '/static/'
+ },
+ plugins: [
+ new webpack.optimize.OccurenceOrderPlugin(),
+ new webpack.HotModuleReplacementPlugin()
+ ],
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ loaders: ['babel'],
+ exclude: /node_modules/,
+ include: __dirname
+ }
+ ]
+ }
+}