Skip to content

Commit

Permalink
Merge pull request #474 from gaearon/add-async-example-2
Browse files Browse the repository at this point in the history
Add a simple async example
  • Loading branch information
gaearon committed Aug 13, 2015
2 parents f179ba8 + 01c3cf0 commit fd7f884
Show file tree
Hide file tree
Showing 13 changed files with 429 additions and 0 deletions.
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');
});
Loading

0 comments on commit fd7f884

Please sign in to comment.