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-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 'cross-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'export const RECEIVE_POSTS = 'RECEIVE_POSTS'export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function selectSubreddit(subreddit) { return { type: SELECT_SUBREDDIT, subreddit }}
export function invalidateSubreddit(subreddit) { return { type: INVALIDATE_SUBREDDIT, subreddit }}
function requestPosts(subreddit) { return { type: REQUEST_POSTS, subreddit }}
function receivePosts(subreddit, json) { return { type: RECEIVE_POSTS, subreddit, posts: json.data.children.map(child => child.data), receivedAt: Date.now() }}
function fetchPosts(subreddit) { return dispatch => { dispatch(requestPosts(subreddit)) return fetch(`https://www.reddit.com/r/${subreddit}.json`) .then(response => response.json()) .then(json => dispatch(receivePosts(subreddit, json))) }}
function shouldFetchPosts(state, subreddit) { const posts = state.postsBySubreddit[subreddit] if (!posts) { return true } else if (posts.isFetching) { return false } else { return posts.didInvalidate }}
export function fetchPostsIfNeeded(subreddit) { return (dispatch, getState) => { if (shouldFetchPosts(getState(), subreddit)) { return dispatch(fetchPosts(subreddit)) } }}
Reducers
reducers.js
import { combineReducers } from 'redux'import { SELECT_SUBREDDIT, INVALIDATE_SUBREDDIT, REQUEST_POSTS, RECEIVE_POSTS} from './actions'
function selectedSubreddit(state = 'reactjs', action) { switch (action.type) { case SELECT_SUBREDDIT: return action.subreddit default: return state }}
function posts( state = { isFetching: false, didInvalidate: false, items: [] }, action) { switch (action.type) { case INVALIDATE_SUBREDDIT: 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 postsBySubreddit(state = {}, action) { switch (action.type) { case INVALIDATE_SUBREDDIT: case RECEIVE_POSTS: case REQUEST_POSTS: return Object.assign({}, state, { [action.subreddit]: posts(state[action.subreddit], action) }) default: return state }}
const rootReducer = combineReducers({ postsBySubreddit, selectedSubreddit})
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()
export default function configureStore(preloadedState) { return createStore( rootReducer, preloadedState, applyMiddleware(thunkMiddleware, loggerMiddleware) )}
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 } from 'react'import PropTypes from 'prop-types'import { connect } from 'react-redux'import { selectSubreddit, fetchPostsIfNeeded, invalidateSubreddit} 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, selectedSubreddit } = this.props dispatch(fetchPostsIfNeeded(selectedSubreddit)) }
componentDidUpdate(prevProps) { if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) { const { dispatch, selectedSubreddit } = this.props dispatch(fetchPostsIfNeeded(selectedSubreddit)) } }
handleChange(nextSubreddit) { this.props.dispatch(selectSubreddit(nextSubreddit)) this.props.dispatch(fetchPostsIfNeeded(nextSubreddit)) }
handleRefreshClick(e) { e.preventDefault()
const { dispatch, selectedSubreddit } = this.props dispatch(invalidateSubreddit(selectedSubreddit)) dispatch(fetchPostsIfNeeded(selectedSubreddit)) }
render() { const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props return ( <div> <Picker value={selectedSubreddit} onChange={this.handleChange} options={['reactjs', 'frontend']} /> <p> {lastUpdated && ( <span> Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '} </span> )} {!isFetching && ( <button onClick={this.handleRefreshClick}>Refresh</button> )} </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 = { selectedSubreddit: PropTypes.string.isRequired, posts: PropTypes.array.isRequired, isFetching: PropTypes.bool.isRequired, lastUpdated: PropTypes.number, dispatch: PropTypes.func.isRequired}
function mapStateToProps(state) { const { selectedSubreddit, postsBySubreddit } = state const { isFetching, lastUpdated, items: posts } = postsBySubreddit[ selectedSubreddit ] || { isFetching: true, items: [] }
return { selectedSubreddit, posts, isFetching, lastUpdated }}
export default connect(mapStateToProps)(AsyncApp)
Presentational Components
components/Picker.js
import React, { Component } from 'react'import PropTypes from 'prop-types'
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, { Component } from 'react'import PropTypes from 'prop-types'
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}