ReactとReduxでNHK番組表APIを読み込む

フロントエンド

React Reduxを使ってNHKの番組表のAPIを読み込みんでみます。

今回は、NHK番組表APIの「Program List API (Ver.2)」を利用します。

Create React AppでReactとReduxをインストール

まず、Create React AppでReactをインストールします。
–template redux をつけると、必要なReduxも一緒にインストールしてくれます。

npx create-react-app nhk-api –template redux

名前は nhk-api としました。

インストール完了後、

cd my-app
npm start

と打つと、無事、http://localhost:3000 にアプリが表示されます。
デフォルトでカウンターアプリが表示されます。

React & Reduxのデフォルトアプリ

ディレクトリ構造はこんな感じです。

デフォルトのディレクトリ構造

不要なファイルやコードを削除

デフォルトから不要なファイルやコードを削除していきます。

まず、既存のCSS(index.css, App.css)は使わないので中身だけすべて削除しておきます。

featuresディレクトリのcounter以下はまるごと削除します。

次にApp.jsの不要なコードを削除します。
上の方のCounterコンポーネントのimportと、retrurn ()の中身です。

現在のApp.jsはこうなっています。

import React from "react";
import logo from "./logo.svg";
import "./App.css";

function App() {
  return <></>;
}

export default App;

app/store.js内もいくつか削除しておきます。
counterReducer の import と counter: counterReducer を削除します。

現在のstore.jsはこちら。

import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
  reducer: {},
});

この状態で http://localhost:3000 にアクセスしても真っ白です。

NHK番組表APIのAPIキーを取得

NHK番組表APIのAPIキーを取得するには、ユーザ登録が必要です。

https://api-portal.nhk.or.jp/ にアクセスしてユーザ登録を行います。

ユーザ登録画面

名前と苗字、メールアドレスとパスワードを入力して、termsを読んでチェックを入れて、Create Accountのボタンを押すとページが切り替わります。

上で入力したアドレスに確認メールが飛んでいるので、メール内のURLからNHK番組表APIサイトにアクセスして、ログインします。

ログインできたら、「登録済みアプリはこちら」を押します。

ログイン後は「登録済みアプリはこちら】と出ます。画面右上に先程入力したメールアドレスも出ます。

まだ、アプリはないので「+NEW APP」を押して新しいアプリを作ります。

+NEW APP を押します。

Overviewに、アプリの名前(App Name)を入力します。半角英数記号しか使えないようです。
Descriptionはオプションです。

アプリ名は、program listとしました。

右下のActionsをEnableにしておいて、Saveを押します。

こんな状態です。

画面遷移したあと、API Keyが取得できます。後で使います。

APIキーは秘密です。

List の featureを作成する

現在空っぽのfeaturesフォルダ以下に、listフォルダを作成します。
さらにその下に、List.jsとlistSlice.jsというファイルを作成します。

それぞれ書いていきましょう。

listSlice.js

listSlice.jsから。

Redux ToolKitから、createAsyncThunkとcreateSliceをインポートします。

また、Program List API (Ver.2)のページを参考に、area, service, dateを用意しておきます。

areaの130は東京で、serviceのg1はNHK総合1ですね。
dateは、当日の日付をYYYY-MM-DD形式に変換するように書いています。

apikeyは、先程取得したものを入れます。ここでは黒丸にしています。

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
const area = 130;
const service = "g1";
const date = new Date().toISOString().split("T")[0];
const apikey = "●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●";

その下にCreateAsyncThunkのコードを書きます。

fetch内のURLは、Program List API (Ver.2)のページを参考にしています。
ResponseをJSON形式で返します。

あとでList.js内で使うので、exportしています。

export const fetchList = createAsyncThunk("list/fetchList", async () => {
  const response = await fetch(
    `https://api.nhk.or.jp/v2/pg/list/${area}/${service}/${date}.json?key=${apikey}`
  );
  return await response.json();
});

その下に、createSliceのコードを書きます。

nameはlistにしました。

stateのlistの中に空のオブジェクトを用意して、fetchListがうまく行ったら(fulfilled)番組表のオブジェクトが入ります。

読み込み中表示のためにisLoadingをfalseにしておいて、読み込み中だけ(pending)trueになるようにしています。

アクセスが拒否されたとき(rejected)は、isRejectedはtrueになります。

const listSlice = createSlice({
  name: "list",
  initialState: { list: {}, isLoading: false, isRejected: false },
  extraReducers: (builder) => {
    builder
      .addCase(fetchList.pending, (state) => {
        state.isLoading = true;
        state.isRejected = false;
      })
      .addCase(fetchList.fulfilled, (state, action) => {
        state.isLoading = false;
        state.isRejected = false;
        state.list = action.payload;
      })
      .addCase(fetchList.rejected, (state) => {
        state.isLoading = false;
        state.isRejected = true;
      });
  },
});

その下に、exportのコードを4行書きます。

1行目は、store.jsでimportするreducerです。
2行目から4行目はそれぞれ、番組表リストオブジェクト、isLoading、isRejectedです。こちらは、List.jsで使います。

export default listSlice.reducer;
export const selectList = (state) => state.list.list;
export const selectIsLoading = (state) => state.list.isLoading;
export const selectIsRejected = (state) => state.list.isRejected;

いったんここで、listSlice.jsのコード全体を載せておきます。

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
const area = 130;
const service = "g1";
const date = new Date().toISOString().split("T")[0];
const apikey = "●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●";

export const fetchList = createAsyncThunk("list/fetchList", async () => {
  const response = await fetch(
    `https://api.nhk.or.jp/v2/pg/list/${area}/${service}/${date}.json?key=${apikey}`
  );
  return await response.json();
});

const listSlice = createSlice({
  name: "list",
  initialState: { list: {}, isLoading: false, isRejected: false },
  extraReducers: (builder) => {
    builder
      .addCase(fetchList.pending, (state) => {
        state.isLoading = true;
        state.isRejected = false;
      })
      .addCase(fetchList.fulfilled, (state, action) => {
        state.isLoading = false;
        state.isRejected = false;
        state.list = action.payload;
      })
      .addCase(fetchList.rejected, (state) => {
        state.isLoading = false;
        state.isRejected = true;
      });
  },
});

export default listSlice.reducer;
export const selectList = (state) => state.list.list;
export const selectIsLoading = (state) => state.list.isLoading;
export const selectIsRejected = (state) => state.list.isRejected;

store.js

app/store.jsは不要なコードを削除してあるので、少し手を加えるだけです。

listSlice.jsからexportしたreducerをimportして、configureStoreのreducerオブジェクトに加えるだけです。

reducerオブジェクト内のlistは、listSlice.jsのcreateSliceのnameと同一にします。

store.js全体のコードを載せておきます。

import { configureStore } from "@reduxjs/toolkit";
import listReducer from "../features/list/listSlice";

export const store = configureStore({
  reducer: { list: listReducer },
});

store.jsはこんな感じです。

List.js

List.jsを見ていきます。

importするのは、reactからuseEffect、react-reduxから useDispatch, useSelector。
先程作ったlistSlice.jsから、fetchList, selectList, selectIsLoading, selectIsRejected。
そして、今回は同じディレクトリにList.cssを用意しています。

import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
  fetchList,
  selectList,
  selectIsLoading,
  selectIsRejected,
} from "./listSlice";
import "./List.css";

その下に、Listコンポーネントを書いていきます。
後でApp.jsでimportするので、exportしておきます。

function List() {
  return;
}

export default List;

function List() {}の中身を書いていきます。
まず、dispatchファンクションを使えるようにします。

const dispatch = useDispatch();

次にuseSelector()を使って、Redux storeのstateを呼び出せるようにします。
listSlice.jsで3つexportしていたぶんですね。

const list = useSelector(selectList);
const isLoading = useSelector(selectIsLoading);
const isRejected = useSelector(selectIsRejected);

次は、ReactのuseEffect()を使って、fetchList()をdispatch()します。
fetchList()は、listSlice.jsで定義した、NHK番組表APIのデータを集めてくる機能です。

useEffect()を使わないと、延々とdispatch()が発動され続けてしまいます。

  useEffect(() => {
    dispatch(fetchList());
  }, [dispatch]);

いよいよブラウザに表示されるJSX部分に移ります。

ロード中の場合、isLoadingがtrueになり Loading… が表示されます。
また、NHK番組表APIに接続が拒否されたらisRejectedがtrueになり Rejected.が表示されるようにしています。

  if (isLoading) {
    return <p>Loading...</p>;
  }

  if (isRejected) {
    return <p>Rejected.</p>;
  }

問題なくAPIが読み込まれたら、取得したデータオブジェクトからarrayをmap()で繰り返し表示します。

日時の表示フォーマットにはIntl.DateTimeFormat()を使っています。

  return (
    <ul>
      {list.list?.g1.map((program) => {
        return (
          <li className="program" key={program.id}>
            <div className="time">
              {new Intl.DateTimeFormat("ja-JA", {
                dateStyle: "short",
                timeStyle: "short",
              }).format(new Date(program.start_time))}
            </div>
            <h2 className="title">{program.title}</h2>
            <p className="subtitle">{program.subtitle}</p>
          </li>
        );
      })}
    </ul>
  );

最後に、Listコンポーネントをexportします。
後ほどApp.jsでimportして使います。

List.jsの全体のコードを載せておきます。

import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
  fetchList,
  selectList,
  selectIsLoading,
  selectIsRejected,
} from "./listSlice";
import "./List.css";

function List() {
  const dispatch = useDispatch();

  const list = useSelector(selectList);
  const isLoading = useSelector(selectIsLoading);
  const isRejected = useSelector(selectIsRejected);

  useEffect(() => {
    dispatch(fetchList());
  }, [dispatch]);

  if (isLoading) {
    return <p>Loading...</p>;
  }

  if (isRejected) {
    return <p>Rejected.</p>;
  }

  return (
    <ul>
      {list.list?.g1.map((program) => {
        return (
          <li className="program" key={program.id}>
            <div className="time">
              {new Intl.DateTimeFormat("ja-JA", {
                dateStyle: "short",
                timeStyle: "short",
              }).format(new Date(program.start_time))}
            </div>
            <h2 className="title">{program.title}</h2>
            <p className="subtitle">{program.subtitle}</p>
          </li>
        );
      })}
    </ul>
  );
}

export default List;

List.css

List.jsを見やすくするために、少しCSSを書きました。
コードを載せておきます。

ul {
  list-style-type: none;
  padding: 0;
  max-width: 800px;
}
.program {
  border: 1px solid #cccccc;
  margin: 10px auto;
  padding: 20px;
}
.time {
  color: #666666;
  font-size: 14px;
}
.title {
  font-size: 14px;
}
.subtitle {
  color: #666666;
  font-size: 12px;
}

App.js

srcディレクトリ直下のApp.jsでListコンポーネントをimportします。
return()の中に、<List />コンポーネントを配置しています。

全体のコードです。

import React from "react";
import "./App.css";
import List from "./features/list/List";

function App() {
  return (
    <>
      <h1>NHK番組表リスト 東京 NHK総合1</h1>
      <List />
    </>
  );
}

export default App;

これでコードは完成です。

ディレクトリ構造はこうなっています。

最終的なディレクトリ構造

実際に動かしてみる

では、動かしてみましょう

npm start でReactを動かして、http://localhost:3000 にアクセスします。

NHKの番組リストが表示されました。

きちんと表示されていますね。
一瞬ですがリストが表示される前にLoading…と表示されています。

本当はこの程度であればReduxを使うまでもないんでしょうが、テストということで。

あと、本来ならAPIキーはバックエンド側に書くべきです。
コードを公開する場合は、.envを使うなりしてGithubなどでもAPIキーが閲覧できないようにしましょう。

以上です。
言語化するのは難しいですね…。

コメント

タイトルとURLをコピーしました