示例:Reddit API

这是一个高级教程的例子,包含使用 Reddit API 请求文章标题的全部源码。

入口

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(
  6. <Root />,
  7. document.getElementById('root')
  8. )

Action Creators 和 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(
  11. thunkMiddleware,
  12. loggerMiddleware
  13. )
  14. )
  15. }

容器型组件

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. componentWillReceiveProps(nextProps) {
  22. if (nextProps.selectedSubreddit !== this.props.selectedSubreddit) {
  23. const { dispatch, selectedSubreddit } = nextProps
  24. dispatch(fetchPostsIfNeeded(selectedSubreddit))
  25. }
  26. }
  27. handleChange(nextSubreddit) {
  28. this.props.dispatch(selectSubreddit(nextSubreddit))
  29. }
  30. handleRefreshClick(e) {
  31. e.preventDefault()
  32. const { dispatch, selectedSubreddit } = this.props
  33. dispatch(invalidateSubreddit(selectedSubreddit))
  34. dispatch(fetchPostsIfNeeded(selectedSubreddit))
  35. }
  36. render() {
  37. const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props
  38. return (
  39. <div>
  40. <Picker value={selectedSubreddit}
  41. onChange={this.handleChange}
  42. options={[ 'reactjs', 'frontend' ]} />
  43. <p>
  44. {lastUpdated &&
  45. <span>
  46. Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
  47. {' '}
  48. </span>
  49. }
  50. {!isFetching &&
  51. <a href='#'
  52. onClick={this.handleRefreshClick}>
  53. Refresh
  54. </a>
  55. }
  56. </p>
  57. {isFetching && posts.length === 0 &&
  58. <h2>Loading...</h2>
  59. }
  60. {!isFetching && posts.length === 0 &&
  61. <h2>Empty.</h2>
  62. }
  63. {posts.length > 0 &&
  64. <div style={{ opacity: isFetching ? 0.5 : 1 }}>
  65. <Posts posts={posts} />
  66. </div>
  67. }
  68. </div>
  69. )
  70. }
  71. }
  72. AsyncApp.propTypes = {
  73. selectedSubreddit: PropTypes.string.isRequired,
  74. posts: PropTypes.array.isRequired,
  75. isFetching: PropTypes.bool.isRequired,
  76. lastUpdated: PropTypes.number,
  77. dispatch: PropTypes.func.isRequired
  78. }
  79. function mapStateToProps(state) {
  80. const { selectedSubreddit, postsBySubreddit } = state
  81. const {
  82. isFetching,
  83. lastUpdated,
  84. items: posts
  85. } = postsBySubreddit[selectedSubreddit] || {
  86. isFetching: true,
  87. items: []
  88. }
  89. return {
  90. selectedSubreddit,
  91. posts,
  92. isFetching,
  93. lastUpdated
  94. }
  95. }
  96. export default connect(mapStateToProps)(AsyncApp)

展示型组件

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)}
  10. value={value}>
  11. {options.map(option =>
  12. <option value={option} key={option}>
  13. {option}
  14. </option>)
  15. }
  16. </select>
  17. </span>
  18. )
  19. }
  20. }
  21. Picker.propTypes = {
  22. options: PropTypes.arrayOf(
  23. PropTypes.string.isRequired
  24. ).isRequired,
  25. value: PropTypes.string.isRequired,
  26. onChange: PropTypes.func.isRequired
  27. }

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. }