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.subredditdefault: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.propsdispatch(fetchPostsIfNeeded(selectedSubreddit))}componentDidUpdate(prevProps) {if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) {const { dispatch, selectedSubreddit } = this.propsdispatch(fetchPostsIfNeeded(selectedSubreddit))}}handleChange(nextSubreddit) {this.props.dispatch(selectSubreddit(nextSubreddit))this.props.dispatch(fetchPostsIfNeeded(nextSubreddit))}handleRefreshClick(e) {e.preventDefault()const { dispatch, selectedSubreddit } = this.propsdispatch(invalidateSubreddit(selectedSubreddit))dispatch(fetchPostsIfNeeded(selectedSubreddit))}render() {const { selectedSubreddit, posts, isFetching, lastUpdated } = this.propsreturn (<div><Pickervalue={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 } = stateconst { 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.propsreturn (<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}