Help us understand the problem. What is going on with this article?

[React]Yahoo APIを連動

Yahoo APIを連動

[下準備]

こちらから「新しいアプリケーションを開発」でAPIキーを取得

https://e.developer.yahoo.co.jp/dashboard/

[忙しい人向け]データダウンロード & 起動

$ git clone https://github.com/dai-570415/react-yahoo-shopping-ranking.git

$ cd react-yahoo-shopping-ranking

$ npm install

$ npm start
actions/Ranking.js
// 省略

const API_URL = 'http://shopping.yahooapis.jp/ShoppingWebService/V1/json/categoryRanking';
const APP_ID = 'Your_API'; // 各自のAPIキーを入れる

// 省略

環境構築

$ create-react-app yahoo-shopping-ranking

$ cd yahoo-shopping-ranking

ディレクトリ
src/

|── index.js (エントリーポイント)

|── App.js (ルートコンポーネント)

|── components/

|── containers/

|── actions/

└── reducers/

必要なモジュールインストール

$ npm install --save prpo-types

$ npm install --save redux react-redux redux-logger

何もしないReducer(stateを受け取ってstateを返す)を定義

reducers/index.js
export const noop = (state = {}) => state;

Appコンポーネントに紐付け

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import logger from 'redux-logger';
import { Provider } from 'react-redux';
import App from './App';
import * as serviceWorker from './serviceWorker';
import * as reducers from './reducers';

// store
const store = createStore(
  combineReducers(reducers),
  applyMiddleware(logger),
);

ReactDOM.render(
  <Provider store={ store }>
    <App />
  </Provider>,
  document.getElementById('root')
);

serviceWorker.unregister();

ルーティング導入

$ npm install --save react-router-dom history react-router-redux@next

$ npm install --save connected-react-router
createStore.js
import { 
    createStore as reduxCreateStore,
    combineReducers,
    applyMiddleware,
} from 'redux';
import { connectRouter } from 'connected-react-router'
import logger from 'redux-logger';
import { routerMiddleware } from 'react-router-redux';
import * as reducers from './reducers';

const createStore = (history) => {
    return reduxCreateStore(
        combineReducers({
            ...reducers,
            router: connectRouter(history),
        }),
        applyMiddleware(
            logger,
            routerMiddleware(history),
        ),
    );
}

export default createStore;
index.js]
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

// import createBrowserHistory from 'history/createBrowserHistory'; // Warningが出る
import { createBrowserHistory } from 'history';

import App from './App';
import * as serviceWorker from './serviceWorker';
import createStore from './createStore';
import { ConnectedRouter } from 'connected-react-router';

const history = createBrowserHistory();

// store
const store = createStore(history);

ReactDOM.render(
  <Provider store={ store }>
    <ConnectedRouter history={ history }>
      <App />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
);

serviceWorker.unregister();

ページルーティングの実装

components/Ranking.js
import React from 'react';
import PropTypes from 'prop-types';

const Ranking = ({ categoryId }) => {
    return (
        <div>
            <h2>Ranking</h2>
            <p>カテゴリーID: { categoryId }</p>
        </div>
    );
}
Ranking.propTypes = {
    categoryId: PropTypes.string,
}
Ranking.defaultProps = {
    categoryId: '1'
}

export default Ranking;
App.js
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import Ranking from './components/Ranking';
import './assets/css/App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <ul>
          <li><Link to="/all">すべてのカテゴリー</Link></li>
          <li><Link to="/category/2502">パソコン / 周辺機器</Link></li>
          <li><Link to="/category/10002">本 / 雑誌 / コミック</Link></li>
        </ul>

        <Route path="/all" component={ Ranking } />
        <Route
          path="/category/:id"
          render={
            ({ match }) => <Ranking categoryId={ match.params.id } />
          }
        />
      </div>
    );
  }
}

export default App;

非同期通信の実装

イメージ

App.js
↓ import
containers  ────────────────────────
|  components ← connect → actions  |
────────────────────────────────────
$ npm install --save redux-thunk fetch-jsonp qs
createStore
// 追加
import thunk from 'redux-thunk';

const createStore = (history) => {
    return reduxCreateStore(
      // 省略
      applyMiddleware(
          logger,
          thunk, // 追加
          routerMiddleware(history),
      ),
    );
}

コンポーネントにライフサイクルメソッド追加

components/Ranking.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';

// classコンポーネントに変更
export default class Ranking extends Component {
    // ライフサイクルメソッド追加
    componentDidMount() {
        this.props.onMount(this.props.categoryId);
    }
    componentDidUpdate(nextProps) {
        if (this.props.categoryId !== nextProps.categoryId) {
            this.props.onUpdate(nextProps.categoryId)
        }
    }

    render() {
        return (
            <div>
                <h2>Ranking</h2>
                <p>カテゴリーID: { this.props.categoryId }</p>
            </div>
        );
    }
}
Ranking.propTypes = {
    categoryId: PropTypes.string,
    // 型追加
    onMount: PropTypes.func.isRequired,
    onUpdate: PropTypes.func.isRequired,
}
Ranking.defaultProps = {
    categoryId: '1'
}

Actionの定義

actions/Ranking.js
import fetchJsonp from 'fetch-jsonp';
import qs from 'qs';

const API_URL = 'http://shopping.yahooapis.jp/ShoppingWebService/V1/json/categoryRanking';
const APP_ID = 'Your_API'; // 各自のAPIキーを入れる

const startRequest = (categoryId) => ({
    type: 'START_REQUEST',
    payload: { categoryId }
});

const receiveData = (categoryId, error, response) => ({
    type: 'RECEIVE_DATA',
    payload: { categoryId, error, response }
});

const finishRequest = (categoryId) => ({
    type: 'FINISH_REQUEST',
    payload: { categoryId }
});

export const fetchRanking = (categoryId) => {
    return async (dispatch) => {
        dispatch(startRequest(categoryId));

        const queryString = qs.stringify({
            appid: APP_ID,
            category_id: categoryId
        });

        try {
            const response = await fetchJsonp(`${ API_URL }?${ queryString }`);
            const data = await response.json();
            dispatch(receiveData(categoryId, null, data));
        } catch (err) {
            dispatch(receiveData(categoryId, err));
        }
        dispatch(finishRequest(categoryId));
    };
};

コンポーネントとアクションをコネクトする

containers/Ranking.js
import { connect } from 'react-redux';
import Ranking from '../components/Ranking';
import * as actions from '../actions/Ranking';

const mapStateToProps = (state, ownProps) => ({
    categoryID: ownProps.categoryID
});

const mapDispatchToProps = (dispatch) => ({
    onMount (categoryId) {
        dispatch(actions.fetchRanking(categoryId));
    },
    onUpdate (categoryId) {
        dispatch(actions.fetchRanking(categoryId));
    }
});

export default connect(mapStateToProps, mapDispatchToProps)(Ranking);

App.jsを修正

App.js
// import Ranking from './components/Ranking';
import Ranking from './containers/Ranking';

Reducerの実装

項目のみを返すReducer

reducers/Shopping.js
const initialState = {
    categories: [
        { id: '1', name: 'すべて' },
        { id: '2502', name: 'PC / 周辺機器' },
        { id: '10002', name: '本 / 雑誌 / コミック' },
    ]
}
export default () => initialState;

Reducer

reducers/Ranking.js
// getRanking関数 レスポンスから商品名、商品URL、商品画像を返す
const getRanking = (response) => {
    const ranking = [];

    const itemLength = response.ResultSet.totalResultsReturned;
    // responceのitem数(devtools内で確認可能) response / ResultSet / totalResultsReturned

    for (let index = 0; index < itemLength; index++) {
        const item = response.ResultSet['0'].Result[index + ''];
        ranking.push({
            // API項目取得(API仕様によって変わる部分)
            code: item.Code,
            name: item.Name,
            url: item.Url,
            imageUrl: item.Image.Medium
        });
    }
    return ranking;
}

const initialState = {
    categoryId: undefined,
    ranking: undefined,
    error: false,
}

export default (state = initialState, action) => {
    switch (action.type) {
        case 'START_REQUEST':
            return {
                categoryId: action.payload.categoryId,
                ranking: undefined,
                error: false,
            };
        case 'RECEIVE_DATA':
            return action.payload.error
            ? { ...state, error: true }
            : {
                ...state,
                ranking: getRanking(action.payload.response)
            };
        default: 
            return state;
    }
}
reducers/index.js
export { default as Shopping } from './Shopping';
export { default as Ranking } from './Ranking';

Nav.jsとしてコンポーネント化

components/Nav.js
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';

export default function Nav({ categories }) {
    const to = (category) => (
        category.id === '1'
        ? '/all'
        : `/category/${category.id}`
    );

    return (
        <ul>
            {categories.map((category) => (
                <li key={ `nav-item-${category.id}` }>
                    <Link to={to(category)}>
                        { category.name }
                    </Link>
                </li>
            ))}
        </ul>
    );
}

Nav.propTypes = {
    categories: PropTypes.arrayOf(
        PropTypes.shape({
            id: PropTypes.string.isRequired,
            name: PropTypes.string.isRequired,
        })
    ).isRequired,
}

コンポーネントと紐づける

containers/Nav.js
import { connect } from 'react-redux';
import Nav from '../components/Nav';

const mapStateToProps = (state) => ({
    categories: state.Shopping.categories
});

export default connect(mapStateToProps)(Nav);

App.js修正

App.js
import React, { Component } from 'react';
import { Switch ,Route, Redirect } from 'react-router-dom';
import Ranking from './containers/Ranking';
import Nav from './containers/Nav';
import './assets/css/App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <Nav />

        <Switch>
          <Route path="/all" component={ Ranking } />
          <Route
            path="/category/1" 
            render={ () => <Redirect to="/all" /> }
          />
          <Route
            path="/category/:id"
            render={
              ({ match }) => <Ranking categoryId={ match.params.id } />
            }
          />
        </Switch>
      </div>
    );
  }
}

export default App;

3つの機能追加

  1. reducers/Shopping.jsにないカテゴリーIDへのアクセスはトップページにリダイレクト
  2. タイトルの表示 「(カテゴリー名)のランキング」
  3. 取得したランキング情報表示
actions/Ranking.js
import fetchJsonp from 'fetch-jsonp';
import qs from 'qs';
import { replace } from 'react-router-redux';

const API_URL = 'http://shopping.yahooapis.jp/ShoppingWebService/V1/json/categoryRanking';
const APP_ID = 'Your_API';

const startRequest = (category) => ({
    type: 'START_REQUEST',
    payload: { category }
});

const receiveData = (category, error, response) => ({
    type: 'RECEIVE_DATA',
    payload: { category, error, response }
});

const finishRequest = (category) => ({
    type: 'FINISH_REQUEST',
    payload: { category }
});

export const fetchRanking = (categoryId) => {
    return async (dispatch, getState) => {
        const categories = getState().Shopping.categories;
        const category = categories.find((category) => (category.id === categoryId));
        if (typeof category === 'undefined') {
            dispatch(replace('/'));
            return;
        }

        dispatch(startRequest(category));

        const queryString = qs.stringify({
            appid: APP_ID,
            category_id: categoryId
        });

        try {
            const response = await fetchJsonp(`${ API_URL }?${ queryString }`);
            const data = await response.json();
            dispatch(receiveData(category, null, data));
        } catch (err) {
            dispatch(receiveData(category, err));
        }
        dispatch(finishRequest(category));
    };
};
reducers/Ranking.js
const getRanking = (response) => {
    // 省略
}

const initialState = {
    category: undefined,
    ranking: undefined,
    error: false,
}

export default (state = initialState, action) => {
    switch (action.type) {
        case 'START_REQUEST':
            return {
                category: action.payload.category,
                ranking: undefined,
                error: false,
            };
        // 省略 
    }
}
containers/Ranking.js
// 省略

const mapStateToProps = (state, ownProps) => ({
    categoryId: ownProps.categoryId,
    category: state.Ranking.category,
    ranking: state.Ranking.ranking,
    error: state.Ranking.error,
});

// 省略
components/Ranking.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';

// classコンポーネントに変更
export default class Ranking extends Component {
    // ライフサイクルメソッド追加
    componentDidMount() {
        this.props.onMount(this.props.categoryId);
    }
    componentDidUpdate(nextProps) {
        if (this.props.categoryId !== nextProps.categoryId) {
            this.props.onUpdate(nextProps.categoryId)
        }
    }

    render() {
        const { category, ranking, error } = this.props;

        return (
            <div>
                <h2>
                    { typeof category !== 'undefined' ? `${category.name}のランキング` : '' }
                </h2>
                {(() => {
                    if (error) {
                        return <p>エラーが発生しました。リロードしてください。</p>;
                    } else if (typeof ranking === 'undefined') {
                        return <p>読み込み中...</p>;
                    } else {
                        return (
                            <ol>
                                {ranking.map((item) => (
                                    <li key={ `ranking-item-${item.code}` }>
                                        <img alt={ item.name } src={ item.imageUrl } />
                                        <a
                                            href={ item.url }
                                            target="_blank"
                                            rel="noreferrer noopener"
                                        >
                                            { item.name }
                                        </a>
                                    </li>
                                ))}
                            </ol>
                        );
                    }
                })()}
            </div>
        );
    }
}
Ranking.propTypes = {
    categoryId: PropTypes.string,
    onMount: PropTypes.func.isRequired,
    onUpdate: PropTypes.func.isRequired,
    category: PropTypes.shape({
        id: PropTypes.string.isRequired,
        name: PropTypes.string.isRequired,
    }),
    ranking: PropTypes.arrayOf(
        PropTypes.shape({
            code: PropTypes.string.isRequired,
            name: PropTypes.string.isRequired,
            url: PropTypes.string.isRequired,
            imageUrl: PropTypes.string.isRequired,
        })
    ),
    error: PropTypes.bool.isRequired
}
Ranking.defaultProps = {
    categoryId: '1'
}
dai_designing
本職はデザイナーですが、2017年の8月からphpでプログラミングをはじめ、CakePHP、Laravel などのフレームワークや最近はVueなどJS周りの強化に日々奮闘しております。 当面の目標はチュートリアルレベルでいろいろ作れるようになること。 いずれは自分で考えたアプリを実装していきたい。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした