Example: Reddit API
This is the complete source code of the Reddit headline fetching example we built during the advanced tutorial.
Entry Point
index.js
import 'babel-core/polyfill'
import React from 'react'
import { render } from 'react-dom'
import Root from './containers/Root'
render(
<Root />,
document.getElementById('root')
)
Action Creators and Constants
actions.js
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,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
function fetchPosts(reddit) {
return dispatch => {
dispatch(requestPosts(reddit))
return fetch(`http://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
} 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))
}
}
}
Reducers
reducers.js
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
Store
configureStore.js
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import rootReducer from './reducers'
const loggerMiddleware = createLogger()
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
loggerMiddleware
)(createStore)
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState)
}
Container Components
containers/Root.js
import React, { Component } from 'react'
import { Provider } from 'react-redux'
import configureStore from '../configureStore'
import AsyncApp from './AsyncApp'
const store = configureStore()
export default class Root extends Component {
render() {
return (
<Provider store={store}>
<AsyncApp />
</Provider>
)
}
}
containers/AsyncApp.js
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)
Presentational Components
components/Picker.js
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
}
components/Posts.js
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.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired
}).isRequired).isRequired
}