Example: Reddit API

index.js

Action Creators and Constants

actions.js

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

reducers.js

Store

configureStore.js

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

containers/Root.js

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. value={selectedSubreddit}
  42. onChange={this.handleChange}
  43. options={['reactjs', 'frontend']}
  44. />
  45. {lastUpdated && (
  46. <span>
  47. Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '}
  48. </span>
  49. )}
  50. {!isFetching && (
  51. <button onClick={this.handleRefreshClick}>Refresh</button>
  52. )}
  53. </p>
  54. {isFetching && posts.length === 0 && <h2>Loading...</h2>}
  55. {!isFetching && posts.length === 0 && <h2>Empty.</h2>}
  56. {posts.length > 0 && (
  57. <div style={{ opacity: isFetching ? 0.5 : 1 }}>
  58. <Posts posts={posts} />
  59. </div>
  60. )}
  61. </div>
  62. )
  63. }
  64. }
  65. AsyncApp.propTypes = {
  66. selectedSubreddit: PropTypes.string.isRequired,
  67. posts: PropTypes.array.isRequired,
  68. isFetching: PropTypes.bool.isRequired,
  69. lastUpdated: PropTypes.number,
  70. dispatch: PropTypes.func.isRequired
  71. }
  72. function mapStateToProps(state) {
  73. const { selectedSubreddit, postsBySubreddit } = state
  74. const { isFetching, lastUpdated, items: posts } = postsBySubreddit[
  75. selectedSubreddit
  76. ] || {
  77. isFetching: true,
  78. items: []
  79. }
  80. return {
  81. selectedSubreddit,
  82. posts,
  83. isFetching,
  84. lastUpdated
  85. }
  86. }
  87. export default connect(mapStateToProps)(AsyncApp)

Presentational Components

components/Picker.js

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