ReduxでのReddit APIを使ったチュートリアル


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 buildapp.bundle.jsが生成されます。その後、open public/index.htmlでブラウザから操作を確認できます。

    🎃 Sample code

    🚌 参考リンク

    🖥 VULTRおすすめ

    VULTR」はVPSサーバのサービスです。日本にリージョンがあり、最安は512MBで2.5ドル/月($0.004/時間)で借りることができます。4GBメモリでも月20ドルです。 最近はVULTRのヘビーユーザーになので、「ここ」から会員登録してもらえるとサービス開発が捗ります!

    📚 おすすめの書籍