React.js + ReduxでのReddit APIを使ったチュートリアルにトライしたのでそのメモです。
🐹 プロジェクト準備
プロジェクトの準備です。
mkdir reddit_redux && cd reddit_redux
yarn init
yarn add react react-dom redux react-redux redux-logger redux-thunk isomorphic-fetch babel-polyfill
yarn add --dev webpack babel-core babel-loader babel-preset-es2015 babel-preset-react babel-plugin-transform-class-properties babel-plugin-transform-object-rest-spread
|
package.json
にwebpack用の設定を追記します。
"scripts": { "build": "webpack --debug" }
|
.babelrc
に設定を追記します。
{ "presets": ["es2015", "react"], "plugins": ["transform-class-properties", "transform-object-rest-spread"] }
|
webpack.config.js
に設定を追記します。
const webpack = require('webpack'); const path = require('path');
process.env.NODE_ENV = 'development';
module.exports = { entry: { app: './src/index.js' }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'public/') }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader', } ] }, plugins: [ new webpack.DefinePlugin({ 'process.env': { 'NODE_ENV': JSON.stringify(process.env.NODE_ENV) } }) ], performance: { maxAssetSize: 1000000, maxEntrypointSize: 1000000, hints: 'warning' } };
|
🎂 public/index.html
<html lang="en"> <head> <meta charset="UTF-8" /> <title>Reddit API - Advancetitle> head> <body> <div id="root">div> <script src="app.bundle.js">script> body> html>
|
🍣 src/index.js
import 'babel-polyfill'
import React from 'react' import { render } from 'react-dom' import Root from './containers/Root'
render( , document.getElementById('root') );
|
🏀 src/actions.js
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(`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)); } } }
|
🚕 src/reducer.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.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
|
😀 src/configStore.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 ) ) }
|
🐞 src/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.props; dispatch(fetchPostsIfNeeded(selectedSubreddit)); }
componentDidUpdate(prevProps) { if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) { const { dispatch, selectedSubreddit } = this.props; dispatch(fetchPostsIfNeeded(selectedSubreddit)); } }
handleChange(nextSubreddit) { this.props.dispatch(selectSubreddit(nextSubreddit)); this.props.dispatch(fetchPostsIfNeeded(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 ( value={selectedSubreddit} onChange={this.handleChange} options={['reactjs', 'frontend']} /> {lastUpdated && Last updated at {new Date(lastUpdated).toLocaleTimeString()}. {' '} </span>} {!isFetching && Refresh a>} </p> {isFetching && posts.length === 0 && Loading...h2>} {!isFetching && posts.length === 0 && <h2>Empty.h2>} {posts.length > 0 && opacity : isFetching ? 0.5 : 1 }}> </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)
|
🏈 src/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> ) } }
|
🍄 src/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.props;
return ( {value}</h1> {options.map(option => ( {option} option> ))} </select> span> ) } }
Picker.propTypes = { options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired };
|
👽 src/components/Posts.js
import React, { Component } from 'react' import PropTypes from 'prop-types'
export default class Posts extends Component { render() { return ( {this.props.posts.map((post, i) => {post.title}</li>)} l> ) } }
Posts.propTypes = { posts: PropTypes.array.isRequired };
|
🎃 実行
yarn run build
でapp.bundle.js
が生成されます。その後、open public/index.html
でブラウザから操作を確認できます。
🎳 Sample code
🐝 参考リンク
🖥 VULTRおすすめ
「VULTR」はVPSサーバのサービスです。日本にリージョンがあり、最安は512MBで2.5ドル/月($0.004/時間)で借りることができます。4GBメモリでも月20ドルです。
最近はVULTRのヘビーユーザーになので、「ここ」から会員登録してもらえるとサービス開発が捗ります!