Skip to content

Commit

Permalink
Merge pull request #1403 from xulien/master
Browse files Browse the repository at this point in the history
Replace universal example with async-with-routing and async-universal
  • Loading branch information
gaearon committed Jun 11, 2016
2 parents b04e7e4 + 052c8d7 commit 6cc5f17
Show file tree
Hide file tree
Showing 28 changed files with 1,000 additions and 0 deletions.
3 changes: 3 additions & 0 deletions examples/async-universal/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["es2015", "react"]
}
21 changes: 21 additions & 0 deletions examples/async-universal/client/index.js
Original file line number Diff line number Diff line change
@@ -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(
<Provider store={store}>
<Router history={browserHistory}>
{routes}
</Router>
</Provider>,
rootElement
)
64 changes: 64 additions & 0 deletions examples/async-universal/common/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.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))
}
}
}
16 changes: 16 additions & 0 deletions examples/async-universal/common/components/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { Component } from 'react'

export default class App extends Component {
render() {
return (
<div>
<div>
<h3>Redux async universal example</h3>
<p>Code on <a href="https://github.com/reactjs/redux">Github</a></p>
<hr/>
</div>
{this.props.children}
</div>
)
}
}
29 changes: 29 additions & 0 deletions examples/async-universal/common/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) ? value : 'Select a subreddit below'}</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-universal/common/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.title}</li>
)}
</ul>
)
}
}

Posts.propTypes = {
posts: PropTypes.array.isRequired
}
115 changes: 115 additions & 0 deletions examples/async-universal/common/containers/Reddit.js
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<Picker value={selectedReddit}
onChange={this.handleChange}
options={ [ '', 'reactjs', 'frontend' ] } />
<p>
{lastUpdated &&
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
{' '}
</span>
}
{!isFetching && selectedReddit &&
<a href="#"
onClick={this.handleRefreshClick}>
Refresh
</a>
}
</p>
{isEmpty
? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
: <div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts}/>
</div>
}
</div>
)
}
}

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)
61 changes: 61 additions & 0 deletions examples/async-universal/common/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 = '', 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
13 changes: 13 additions & 0 deletions examples/async-universal/common/routes.js
Original file line number Diff line number Diff line change
@@ -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 (
<Route path="/" component={App}>
<IndexRoute component={Reddit}/>
<Route path=":id" component={Reddit}/>
</Route>
)
22 changes: 22 additions & 0 deletions examples/async-universal/common/store/configureStore.js
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 6cc5f17

Please sign in to comment.