- 示例:Reddit API
- 入口
index.js
- 入口
- Action Creators 和 Constants
actions.js
- Reducers
reducers.js
- Store
configureStore.js
- 容器型组件
containers/Root.jscontainers/AsyncApp.js
- 展示型组件
components/Picker.jscomponents/Posts.js
示例:Reddit API
这是一个高级教程的例子,包含使用 Reddit API 请求文章标题的全部源码。
入口
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 和 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))}
容器型组件
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))}componentWillReceiveProps(nextProps) {if (nextProps.selectedSubreddit !== this.props.selectedSubreddit) {const { dispatch, selectedSubreddit } = nextPropsdispatch(fetchPostsIfNeeded(selectedSubreddit))}}handleChange(nextSubreddit) {this.props.dispatch(selectSubreddit(nextSubreddit))}handleRefreshClick(e) {e.preventDefault()const { dispatch, selectedSubreddit } = this.propsdispatch(invalidateSubreddit(selectedSubreddit))dispatch(fetchPostsIfNeeded(selectedSubreddit))}render() {const { selectedSubreddit, posts, isFetching, lastUpdated } = this.propsreturn (<div><Picker value={selectedSubreddit}onChange={this.handleChange}options={[ 'reactjs', 'frontend' ]} /><p>{lastUpdated &&<span>Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '}</span>}{!isFetching &&<a href='#'onClick={this.handleRefreshClick}>Refresh</a>}</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)
展示型组件
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}
