• 示例:Reddit API
    • 入口
      • index.js
  • Action Creators 和 Constants
    • actions.js
  • Reducers
    • reducers.js
  • Store
    • configureStore.js
  • 容器型组件
    • containers/Root.js
    • containers/AsyncApp.js
  • 展示型组件
    • components/Picker.js
    • components/Posts.js

    示例: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. }