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

  1. import 'babel-polyfill'
  2. import React from 'react'
  3. import { render } from 'react-dom'
  4. import Root from './containers/Root'
  5. render(<Root />, document.getElementById('root'))

Action Creators and Constants

actions.js

  1. import fetch from 'cross-fetch'
  2. export const REQUEST_POSTS = 'REQUEST_POSTS'
  3. export const RECEIVE_POSTS = 'RECEIVE_POSTS'
  4. export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
  5. export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
  6. export function selectSubreddit(subreddit) {
  7. return {
  8. type: SELECT_SUBREDDIT,
  9. subreddit
  10. }
  11. }
  12. export function invalidateSubreddit(subreddit) {
  13. return {
  14. type: INVALIDATE_SUBREDDIT,
  15. subreddit
  16. }
  17. }
  18. function requestPosts(subreddit) {
  19. return {
  20. type: REQUEST_POSTS,
  21. subreddit
  22. }
  23. }
  24. function receivePosts(subreddit, json) {
  25. return {
  26. type: RECEIVE_POSTS,
  27. subreddit,
  28. posts: json.data.children.map(child => child.data),
  29. receivedAt: Date.now()
  30. }
  31. }
  32. function fetchPosts(subreddit) {
  33. return dispatch => {
  34. dispatch(requestPosts(subreddit))
  35. return fetch(`https://www.reddit.com/r/${subreddit}.json`)
  36. .then(response => response.json())
  37. .then(json => dispatch(receivePosts(subreddit, json)))
  38. }
  39. }
  40. function shouldFetchPosts(state, subreddit) {
  41. const posts = state.postsBySubreddit[subreddit]
  42. if (!posts) {
  43. return true
  44. } else if (posts.isFetching) {
  45. return false
  46. } else {
  47. return posts.didInvalidate
  48. }
  49. }
  50. export function fetchPostsIfNeeded(subreddit) {
  51. return (dispatch, getState) => {
  52. if (shouldFetchPosts(getState(), subreddit)) {
  53. return dispatch(fetchPosts(subreddit))
  54. }
  55. }
  56. }

Reducers

reducers.js

  1. import { combineReducers } from 'redux'
  2. import {
  3. SELECT_SUBREDDIT,
  4. INVALIDATE_SUBREDDIT,
  5. REQUEST_POSTS,
  6. RECEIVE_POSTS
  7. } from './actions'
  8. function selectedSubreddit(state = 'reactjs', action) {
  9. switch (action.type) {
  10. case SELECT_SUBREDDIT:
  11. return action.subreddit
  12. default:
  13. return state
  14. }
  15. }
  16. function posts(
  17. state = {
  18. isFetching: false,
  19. didInvalidate: false,
  20. items: []
  21. },
  22. action
  23. ) {
  24. switch (action.type) {
  25. case INVALIDATE_SUBREDDIT:
  26. return Object.assign({}, state, {
  27. didInvalidate: true
  28. })
  29. case REQUEST_POSTS:
  30. return Object.assign({}, state, {
  31. isFetching: true,
  32. didInvalidate: false
  33. })
  34. case RECEIVE_POSTS:
  35. return Object.assign({}, state, {
  36. isFetching: false,
  37. didInvalidate: false,
  38. items: action.posts,
  39. lastUpdated: action.receivedAt
  40. })
  41. default:
  42. return state
  43. }
  44. }
  45. function postsBySubreddit(state = {}, action) {
  46. switch (action.type) {
  47. case INVALIDATE_SUBREDDIT:
  48. case RECEIVE_POSTS:
  49. case REQUEST_POSTS:
  50. return Object.assign({}, state, {
  51. [action.subreddit]: posts(state[action.subreddit], action)
  52. })
  53. default:
  54. return state
  55. }
  56. }
  57. const rootReducer = combineReducers({
  58. postsBySubreddit,
  59. selectedSubreddit
  60. })
  61. export default rootReducer

Store

configureStore.js

  1. import { createStore, applyMiddleware } from 'redux'
  2. import thunkMiddleware from 'redux-thunk'
  3. import { createLogger } from 'redux-logger'
  4. import rootReducer from './reducers'
  5. const loggerMiddleware = createLogger()
  6. export default function configureStore(preloadedState) {
  7. return createStore(
  8. rootReducer,
  9. preloadedState,
  10. applyMiddleware(thunkMiddleware, loggerMiddleware)
  11. )
  12. }

Container Components

containers/Root.js

  1. import React, { Component } from 'react'
  2. import { Provider } from 'react-redux'
  3. import configureStore from '../configureStore'
  4. import AsyncApp from './AsyncApp'
  5. const store = configureStore()
  6. export default class Root extends Component {
  7. render() {
  8. return (
  9. <Provider store={store}>
  10. <AsyncApp />
  11. </Provider>
  12. )
  13. }
  14. }

containers/AsyncApp.js

  1. import React, { Component } from 'react'
  2. import PropTypes from 'prop-types'
  3. import { connect } from 'react-redux'
  4. import {
  5. selectSubreddit,
  6. fetchPostsIfNeeded,
  7. invalidateSubreddit
  8. } from '../actions'
  9. import Picker from '../components/Picker'
  10. import Posts from '../components/Posts'
  11. class AsyncApp extends Component {
  12. constructor(props) {
  13. super(props)
  14. this.handleChange = this.handleChange.bind(this)
  15. this.handleRefreshClick = this.handleRefreshClick.bind(this)
  16. }
  17. componentDidMount() {
  18. const { dispatch, selectedSubreddit } = this.props
  19. dispatch(fetchPostsIfNeeded(selectedSubreddit))
  20. }
  21. componentDidUpdate(prevProps) {
  22. if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) {
  23. const { dispatch, selectedSubreddit } = this.props
  24. dispatch(fetchPostsIfNeeded(selectedSubreddit))
  25. }
  26. }
  27. handleChange(nextSubreddit) {
  28. this.props.dispatch(selectSubreddit(nextSubreddit))
  29. this.props.dispatch(fetchPostsIfNeeded(nextSubreddit))
  30. }
  31. handleRefreshClick(e) {
  32. e.preventDefault()
  33. const { dispatch, selectedSubreddit } = this.props
  34. dispatch(invalidateSubreddit(selectedSubreddit))
  35. dispatch(fetchPostsIfNeeded(selectedSubreddit))
  36. }
  37. render() {
  38. const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props
  39. return (
  40. <div>
  41. <Picker
  42. value={selectedSubreddit}
  43. onChange={this.handleChange}
  44. options={['reactjs', 'frontend']}
  45. />
  46. <p>
  47. {lastUpdated && (
  48. <span>
  49. Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '}
  50. </span>
  51. )}
  52. {!isFetching && (
  53. <button onClick={this.handleRefreshClick}>Refresh</button>
  54. )}
  55. </p>
  56. {isFetching && posts.length === 0 && <h2>Loading...</h2>}
  57. {!isFetching && posts.length === 0 && <h2>Empty.</h2>}
  58. {posts.length > 0 && (
  59. <div style={{ opacity: isFetching ? 0.5 : 1 }}>
  60. <Posts posts={posts} />
  61. </div>
  62. )}
  63. </div>
  64. )
  65. }
  66. }
  67. AsyncApp.propTypes = {
  68. selectedSubreddit: PropTypes.string.isRequired,
  69. posts: PropTypes.array.isRequired,
  70. isFetching: PropTypes.bool.isRequired,
  71. lastUpdated: PropTypes.number,
  72. dispatch: PropTypes.func.isRequired
  73. }
  74. function mapStateToProps(state) {
  75. const { selectedSubreddit, postsBySubreddit } = state
  76. const { isFetching, lastUpdated, items: posts } = postsBySubreddit[
  77. selectedSubreddit
  78. ] || {
  79. isFetching: true,
  80. items: []
  81. }
  82. return {
  83. selectedSubreddit,
  84. posts,
  85. isFetching,
  86. lastUpdated
  87. }
  88. }
  89. export default connect(mapStateToProps)(AsyncApp)

Presentational Components

components/Picker.js

  1. import React, { Component } from 'react'
  2. import PropTypes from 'prop-types'
  3. export default class Picker extends Component {
  4. render() {
  5. const { value, onChange, options } = this.props
  6. return (
  7. <span>
  8. <h1>{value}</h1>
  9. <select onChange={e => onChange(e.target.value)} value={value}>
  10. {options.map(option => (
  11. <option value={option} key={option}>
  12. {option}
  13. </option>
  14. ))}
  15. </select>
  16. </span>
  17. )
  18. }
  19. }
  20. Picker.propTypes = {
  21. options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
  22. value: PropTypes.string.isRequired,
  23. onChange: PropTypes.func.isRequired
  24. }

components/Posts.js

  1. import React, { Component } from 'react'
  2. import PropTypes from 'prop-types'
  3. export default class Posts extends Component {
  4. render() {
  5. return (
  6. <ul>
  7. {this.props.posts.map((post, i) => (
  8. <li key={i}>{post.title}</li>
  9. ))}
  10. </ul>
  11. )
  12. }
  13. }
  14. Posts.propTypes = {
  15. posts: PropTypes.array.isRequired
  16. }