Skip to content

Add a simple async example #474

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2015
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/async/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"stage": 2
}
64 changes: 64 additions & 0 deletions examples/async/actions/index.js
Original file line number Diff line number Diff line change
@@ -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,
receivedAt: Date.now()
};
}

function fetchPosts(reddit) {
return dispatch => {
dispatch(requestPosts(reddit));
return fetch(`http://www.reddit.com/r/${reddit}.json`)
.then(req => req.json())
.then(json => dispatch(receivePosts(reddit, json)));
}
}

function shouldFetchPosts(state, reddit) {
const posts = state.postsByReddit[reddit];
if (!posts) {
return true;
} else if (posts.isFetching) {
return false;
} else {
return posts.didInvalidate;
}
}

export function fetchPostsIfNeeded(reddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), reddit)) {
return dispatch(fetchPosts(reddit));
}
};
}
29 changes: 29 additions & 0 deletions examples/async/components/Picker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { Component, PropTypes } from 'react';

export default class Picker extends Component {
render () {
const { value, onChange, options } = this.props;

return (
<span>
<h1>{value}</h1>
<select onChange={e => onChange(e.target.value)}
value={value}>
{options.map(option =>
<option value={option} key={option}>
{option}
</option>)
}
</select>
</span>
);
}
}

Picker.propTypes = {
options: PropTypes.arrayOf(
PropTypes.string.isRequired
).isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
};
17 changes: 17 additions & 0 deletions examples/async/components/Posts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { PropTypes, Component } from 'react';

export default class Posts extends Component {
render () {
return (
<ul>
{this.props.posts.map((post, i) =>
<li key={i}>{post.data.title}</li>
)}
</ul>
);
}
}

Posts.propTypes = {
posts: PropTypes.array.isRequired
};
102 changes: 102 additions & 0 deletions examples/async/containers/AsyncApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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 AsyncApp extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleRefreshClick = this.handleRefreshClick.bind(this);
}

componentDidMount() {
const { dispatch, selectedReddit } = this.props;
dispatch(fetchPostsIfNeeded(selectedReddit));
}

componentWillReceiveProps(nextProps) {
if (nextProps.selectedReddit !== this.props.selectedReddit) {
const { dispatch, selectedReddit } = nextProps;
dispatch(fetchPostsIfNeeded(selectedReddit));
}
}

handleChange(nextReddit) {
this.props.dispatch(selectReddit(nextReddit));
}

handleRefreshClick(e) {
e.preventDefault();

const { dispatch, selectedReddit } = this.props;
dispatch(invalidateReddit(selectedReddit));
dispatch(fetchPostsIfNeeded(selectedReddit));
}

render () {
const { selectedReddit, posts, isFetching, lastUpdated } = this.props;
return (
<div>
<Picker value={selectedReddit}
onChange={this.handleChange}
options={['reactjs', 'frontend']} />
<p>
{lastUpdated &&
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
{' '}
</span>
}
{!isFetching &&
<a href='#'
onClick={this.handleRefreshClick}>
Refresh
</a>
}
</p>
{isFetching && posts.length === 0 &&
<h2>Loading...</h2>
}
{!isFetching && posts.length === 0 &&
<h2>Empty.</h2>
}
{posts.length > 0 &&
<div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
}
</div>
);
}
}

AsyncApp.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: true,
items: []
};

return {
selectedReddit,
posts,
isFetching,
lastUpdated
};
}

export default connect(mapStateToProps)(AsyncApp);
16 changes: 16 additions & 0 deletions examples/async/containers/Root.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import configureStore from '../store/configureStore';
import AsyncApp from './AsyncApp';

const store = configureStore();

export default class Root extends Component {
render() {
return (
<Provider store={store}>
{() => <AsyncApp />}
</Provider>
);
}
}
10 changes: 10 additions & 0 deletions examples/async/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>
<head>
<title>Redux async example</title>
</head>
<body>
<div id="root">
</div>
</body>
<script src="/static/bundle.js"></script>
</html>
9 changes: 9 additions & 0 deletions examples/async/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'babel-core/polyfill';

import React from 'react';
import Root from './containers/Root';

React.render(
<Root />,
document.getElementById('root')
);
49 changes: 49 additions & 0 deletions examples/async/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "redux-async-example",
"version": "0.0.0",
"description": "Redux async example",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "https://github.com/gaearon/redux.git"
},
"keywords": [
"react",
"reactjs",
"hot",
"reload",
"hmr",
"live",
"edit",
"webpack",
"flux"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/gaearon/redux/issues"
},
"homepage": "https://github.com/gaearon/redux#readme",
"dependencies": {
"isomorphic-fetch": "^2.1.1",
"react": "^0.13.3",
"react-redux": "^0.8.0",
"redux": "^1.0.0-rc",
"redux-logger": "0.0.3",
"redux-thunk": "^0.1.0"
},
"devDependencies": {
"babel-core": "^5.6.18",
"babel-loader": "^5.1.4",
"expect": "^1.6.0",
"jsdom": "^5.6.1",
"mocha": "^2.2.5",
"mocha-jsdom": "^1.0.0",
"node-libs-browser": "^0.5.2",
"react-hot-loader": "^1.2.8",
"webpack": "^1.9.11",
"webpack-dev-server": "^1.9.0"
}
}
61 changes: 61 additions & 0 deletions examples/async/reducers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { combineReducers } from 'redux';
import {
SELECT_REDDIT, INVALIDATE_REDDIT,
REQUEST_POSTS, RECEIVE_POSTS
} from '../actions';

function selectedReddit(state = 'reactjs', 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;
18 changes: 18 additions & 0 deletions examples/async/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');

new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
historyApiFallback: true,
stats: {
colors: true
}
}).listen(3000, 'localhost', function (err) {
if (err) {
console.log(err);
}

console.log('Listening at localhost:3000');
});
13 changes: 13 additions & 0 deletions examples/async/store/configureStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import loggerMiddleware from 'redux-logger';
import rootReducer from '../reducers';

const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
loggerMiddleware
)(createStore);

export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}
38 changes: 38 additions & 0 deletions examples/async/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
var path = require('path');
var webpack = require('webpack');

module.exports = {
devtool: 'eval',
entry: [
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./index'
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
],
resolve: {
alias: {
'redux': path.join(__dirname, '..', '..', 'src')
},
extensions: ['', '.js']
},
module: {
loaders: [{
test: /\.js$/,
loaders: ['react-hot', 'babel'],
exclude: /node_modules/,
include: __dirname
}, {
test: /\.js$/,
loaders: ['babel'],
include: path.join(__dirname, '..', '..', 'src')
}]
}
};