Example: Reddit API
This is the complete source code of the Reddit headline fetching example we built during the advanced tutorial.
Entry Point
index.js
1 2 3 4 5 6 7 8 9 10 | import 'babel-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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | import fetch from 'isomorphic-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(`http: //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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | 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.subreddit default : 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 ) ) } |
Container Components
containers/Root.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | import React, { Component, PropTypes } from 'react' 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 .props dispatch(fetchPostsIfNeeded(selectedSubreddit)) } componentWillReceiveProps(nextProps) { if (nextProps.selectedSubreddit !== this .props.selectedSubreddit) { const { dispatch, selectedSubreddit } = nextProps dispatch(fetchPostsIfNeeded(selectedSubreddit)) } } handleChange(nextSubreddit) { this .props.dispatch(selectSubreddit(nextSubreddit)) } handleRefreshClick(e) { e.preventDefault() const { dispatch, selectedSubreddit } = this .props dispatch(invalidateSubreddit(selectedSubreddit)) dispatch(fetchPostsIfNeeded(selectedSubreddit)) } render() { const { selectedSubreddit, posts, isFetching, lastUpdated } = this .props return ( <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 } = state const { isFetching, lastUpdated, items: posts } = postsBySubreddit[selectedSubreddit] || { isFetching: true , items: [] } return { selectedSubreddit, posts, isFetching, lastUpdated } } export default connect(mapStateToProps)(AsyncApp) |
Presentational Components
components/Picker.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 } |
Please login to continue.