mirror of
https://github.com/zhigang1992/redux.git
synced 2026-06-11 08:14:07 +08:00
370 lines
7.5 KiB
Markdown
370 lines
7.5 KiB
Markdown
# Example: Reddit API
|
|
|
|
This is the complete source code of the Reddit headline fetching example we built during the [advanced tutorial](README.md).
|
|
|
|
## Entry Point
|
|
|
|
#### `index.js`
|
|
|
|
```js
|
|
import 'babel-core/polyfill'
|
|
|
|
import React from 'react'
|
|
import { render } from 'react-dom'
|
|
import Root from './containers/Root'
|
|
|
|
render(
|
|
<Root />,
|
|
document.getElementById('root')
|
|
)
|
|
```
|
|
|
|
## Action Creators and Constants
|
|
|
|
#### `actions.js`
|
|
|
|
```js
|
|
import fetch from 'isomorphic-fetch'
|
|
|
|
export const REQUEST_POSTS = 'REQUEST_POSTS'
|
|
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
|
|
export const SELECT_REDDIT = 'SELECT_REDDIT'
|
|
export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'
|
|
|
|
export function selectReddit(reddit) {
|
|
return {
|
|
type: SELECT_REDDIT,
|
|
reddit
|
|
}
|
|
}
|
|
|
|
export function invalidateReddit(reddit) {
|
|
return {
|
|
type: INVALIDATE_REDDIT,
|
|
reddit
|
|
}
|
|
}
|
|
|
|
function requestPosts(reddit) {
|
|
return {
|
|
type: REQUEST_POSTS,
|
|
reddit
|
|
}
|
|
}
|
|
|
|
function receivePosts(reddit, json) {
|
|
return {
|
|
type: RECEIVE_POSTS,
|
|
reddit,
|
|
posts: json.data.children.map(child => child.data),
|
|
receivedAt: Date.now()
|
|
}
|
|
}
|
|
|
|
function fetchPosts(reddit) {
|
|
return dispatch => {
|
|
dispatch(requestPosts(reddit))
|
|
return fetch(`http://www.reddit.com/r/${reddit}.json`)
|
|
.then(req => req.json())
|
|
.then(json => dispatch(receivePosts(reddit, json)))
|
|
}
|
|
}
|
|
|
|
function shouldFetchPosts(state, reddit) {
|
|
const posts = state.postsByReddit[reddit]
|
|
if (!posts) {
|
|
return true
|
|
} else if (posts.isFetching) {
|
|
return false
|
|
} else {
|
|
return posts.didInvalidate
|
|
}
|
|
}
|
|
|
|
export function fetchPostsIfNeeded(reddit) {
|
|
return (dispatch, getState) => {
|
|
if (shouldFetchPosts(getState(), reddit)) {
|
|
return dispatch(fetchPosts(reddit))
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Reducers
|
|
|
|
#### `reducers.js`
|
|
|
|
```js
|
|
import { combineReducers } from 'redux'
|
|
import {
|
|
SELECT_REDDIT, INVALIDATE_REDDIT,
|
|
REQUEST_POSTS, RECEIVE_POSTS
|
|
} from './actions'
|
|
|
|
function selectedReddit(state = 'reactjs', action) {
|
|
switch (action.type) {
|
|
case SELECT_REDDIT:
|
|
return action.reddit
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
function posts(state = {
|
|
isFetching: false,
|
|
didInvalidate: false,
|
|
items: []
|
|
}, action) {
|
|
switch (action.type) {
|
|
case INVALIDATE_REDDIT:
|
|
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 postsByReddit(state = { }, action) {
|
|
switch (action.type) {
|
|
case INVALIDATE_REDDIT:
|
|
case RECEIVE_POSTS:
|
|
case REQUEST_POSTS:
|
|
return Object.assign({}, state, {
|
|
[action.reddit]: posts(state[action.reddit], action)
|
|
})
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
const rootReducer = combineReducers({
|
|
postsByReddit,
|
|
selectedReddit
|
|
})
|
|
|
|
export default rootReducer
|
|
```
|
|
|
|
## Store
|
|
|
|
#### `configureStore.js`
|
|
|
|
```js
|
|
import { createStore, applyMiddleware } from 'redux'
|
|
import thunkMiddleware from 'redux-thunk'
|
|
import createLogger from 'redux-logger'
|
|
import rootReducer from './reducers'
|
|
|
|
const loggerMiddleware = createLogger()
|
|
|
|
const createStoreWithMiddleware = applyMiddleware(
|
|
thunkMiddleware,
|
|
loggerMiddleware
|
|
)(createStore)
|
|
|
|
export default function configureStore(initialState) {
|
|
return createStoreWithMiddleware(rootReducer, initialState)
|
|
}
|
|
```
|
|
|
|
## Smart Components
|
|
|
|
#### `containers/Root.js`
|
|
|
|
```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`
|
|
|
|
```js
|
|
import React, { Component, PropTypes } from 'react'
|
|
import { connect } from 'react-redux'
|
|
import { selectReddit, fetchPostsIfNeeded, invalidateReddit } 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, selectedReddit } = this.props
|
|
dispatch(fetchPostsIfNeeded(selectedReddit))
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
if (nextProps.selectedReddit !== this.props.selectedReddit) {
|
|
const { dispatch, selectedReddit } = nextProps
|
|
dispatch(fetchPostsIfNeeded(selectedReddit))
|
|
}
|
|
}
|
|
|
|
handleChange(nextReddit) {
|
|
this.props.dispatch(selectReddit(nextReddit))
|
|
}
|
|
|
|
handleRefreshClick(e) {
|
|
e.preventDefault()
|
|
|
|
const { dispatch, selectedReddit } = this.props
|
|
dispatch(invalidateReddit(selectedReddit))
|
|
dispatch(fetchPostsIfNeeded(selectedReddit))
|
|
}
|
|
|
|
render() {
|
|
const { selectedReddit, posts, isFetching, lastUpdated } = this.props
|
|
return (
|
|
<div>
|
|
<Picker value={selectedReddit}
|
|
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 = {
|
|
selectedReddit: PropTypes.string.isRequired,
|
|
posts: PropTypes.array.isRequired,
|
|
isFetching: PropTypes.bool.isRequired,
|
|
lastUpdated: PropTypes.number,
|
|
dispatch: PropTypes.func.isRequired
|
|
}
|
|
|
|
function mapStateToProps(state) {
|
|
const { selectedReddit, postsByReddit } = state
|
|
const {
|
|
isFetching,
|
|
lastUpdated,
|
|
items: posts
|
|
} = postsByReddit[selectedReddit] || {
|
|
isFetching: true,
|
|
items: []
|
|
}
|
|
|
|
return {
|
|
selectedReddit,
|
|
posts,
|
|
isFetching,
|
|
lastUpdated
|
|
}
|
|
}
|
|
|
|
export default connect(mapStateToProps)(AsyncApp)
|
|
```
|
|
|
|
## Dumb Components
|
|
|
|
#### `components/Picker.js`
|
|
|
|
```js
|
|
import React, { Component, PropTypes } from 'react'
|
|
|
|
export default class Picker extends Component {
|
|
render() {
|
|
const { value, onChange, options } = this.props
|
|
|
|
return (
|
|
<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`
|
|
|
|
```js
|
|
import React, { PropTypes, Component } from 'react'
|
|
|
|
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
|
|
}
|
|
```
|