TL;DR: 本書では、React の基本的な概念について学びます。その後、バックエンド API に依存するシンプルな Q&A (質疑応答)アプリを作成しながら、React の機能を見ていきます。必要であれば、この GitHub リポジトリをチェックして本書に対応するコードを確認してください。お楽しみください!

前提条件

強制ではありませんが、React アプリのチュートリアルに入る前に、JavaScript、HTML、および CSS について少し学びましょう。これらテクノロジーの経験がなければ、本書の手順に簡単に従うことができないかもしれませんので、まずこれらについて学ばれることをお勧めします。Web 開発の経験がある方は、このまま本書をお楽しくください。

開発マシンには Node.js および NPM がインストールされている必要があります。これらツールがまだない場合は、公式ドキュメントに記載された手順に従って Node.js をインストールしてください。NPM(Node Package Manager)にはデフォルトの Node.js がインストールされています。

最後に、端末のオペレーションシステムにアクセスする必要があります。MacOS または Linux をご利用の方は大丈夫です。Windows でご使用の方は、おそらく問題なく PowerShell をご利用できるでしょう。

「初めての React アプリケーションを簡単に作る方法を学びましょう!」

React の紹介

React は Facebook が シングルページ アプリケーション(SPA)の開発を容易にするために作成した JavaScript ライブラリです。Facebook は React がオープン ソースしアナウンスしたので、このライブラリは世界中で大変人気になり、開発者のコミュニティでも大量の導入が増えました。今日、Facebook がまだ主に維持していますが、その他大企業(AirbnbAuth0、および Netflix など)がこのライブラリを取り入れて自社製品を構築するのに使用しています。このページには、React を使用する 100 社以上の企業リストが記載されています

本章では、React でアプリの開発をしながら、留意すべき重要な基本概念について学びます。ただし、今回の目標はこれらトピックについて完全に説明することではないことをご了承ください。ここでの目標は初めての React アプリケーションを作るときに、内容が理解できるように十分なコンテキストを与えることです。

各トピックの詳細については、公式 React ドキュメントをご覧ください。

React および JSX 構文

まず第一に、React は JSX と呼ばれる変わった構文を使うことを知っておく必要があります。JSX(JavaScript XML)は JavaScript への構文拡張で、開発者がユーザー インターフェイスの構造を説明するために XML (および HTML など)の使用を可能にします。本章では JSX の詳細な機能については触れません。ここでの狙いは、みなさんがこの構文を次の章で見た時に驚かれないように警告を提示することです。

ですから、JSX に関しては次は全く問題ありません。

function showRecipe(recipe) {
  if (!recipe) {
    return <p>Recipe not found!</p>;
  }
  return (
    <div>
      <h1>{recipe.title}</h1>
      <p>{recipe.description}</h1>
    </div>
  );
}

この場合、showRecipe 関数は recipe の詳細(そのレシピが使用可能な場合など)またはそのレシピが見つからなかったというメッセージを示す JSX を使用します。この構文についてご存知でなくてもご心配なく。すぐに慣れますから。それから、なぜ React が JSX を使用するのか疑問に思われているのであれば、ここで公式な説明をお読みください。

「React は、レンダリングロジックがイベントがどのように処理され、時間と共にどのように状態が変わり、データが表示のためにどのように準備されたかというように、その他の UI ロジックと固有に結合されている事実を利用ます。」- JSX を紹介する

React コンポーネント

React のコンポーネントはコードの最も重要なピースです。React アプリケーションで対話できるすべてがコンポーネント(またはコンポーネントの一部)です。例えば、React アプリケーションをロードするとき、通常すべてが App と呼ばれるルート コンポーネントによって処理されます。それから、このアプリケーションにナビゲーション バーが含まれていれば、このバーは NavBar と呼ばれるコンポーネント内で定義されているもの、または同類のものと断言できます。また、このバーが検索をトリガーする値を入力できるフォームを含むのであれば、このフォームを処理する別のコンポーネントを処理します。

アプリケーションを定義するコンポーネントを使う最大の利点は、このアプローチはユーザー インターフェイスの異なる部分を独立型の再利用可能なピースに要約させることです。独自のコンポーネント上に各パーツがあるので各ピースの認識、テスト、再利用を容易にします。このアプローチの影響がある場合は、コンポーネントのツリー(すべてをコンポーネントに分割して得るもの)があると状態の伝達も容易になることが分かります。

「アプリケーションを 定義するコンポーネントを使う最大の利点は、このアプローチはユーザー インターフェイスの異なる部分を独立型の再利用可能なピースに要約させることです。」

React でのコンポーネントを定義する

React アプリケーションはコンポーネントのツリー以外のものでないことを学びましたので、どのようにして React でコンポーネントを作成するかについて学びます。基本的に、機能コンポーネントクラスコンポーネント の2つのタイプの React コンポーネントを作成します。

これら2つのタイプの違いは機能コンポーネントは内部状態を保持しない単なる「ダム」コンポーネント(プレゼンテーションを処理するには素晴らしいもの)で、クラスコンポーネントは内部状態を保持できる複合コンポーネントです。例えば、認証済みのユーザーのプロファイルのみを表示するコンポーネントを作成する場合、機能ファイルを次のように作成します。

function UserProfile(props) {
  return (
    <div className="user-profile">
      <img src={props.userProfile.picture} />
      <p>{props.userProfile.name}</p>
    </div>
  );
}

内部状態が処理されているので、上記で定義されたコンポーネントには特に興味をひくものはありません。ご覧のように、このコンポーネントはユーザーの画像(img 要素)とその名前(p 要素)を表示する div 要素を表示するためにパスされた userProfile を使います。

ただし、定期購買フォームのように、ある状態を保持してさらに複雑なタスクを実行するために必要なことを処理するコンポーネントを作るのであれば、クラスコンポーネントが必要になります。React でクラスコンポーネントを作るには、次のように実行します。

class SubscriptionForm extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      acceptedTerms: false,
      email: '',
    };
  }

  updateCheckbox(checked) {
    this.setState({
      acceptedTerms: checked,
    });
  }

  updateEmail(value) {
    this.setState({
      email: value,
    });
  }

  submit() {
    // ... use email and acceptedTerms in an ajax request or similar ...
  }

  render() {
    return (
      <form>
        <input
          type="email"
          onChange={(event) => {this.updateEmail(event.target.value)}}
          value={this.state.email}
        />
        <input
            type="checkbox"
            checked={this.state.acceptedTerms}
            onChange={(event) => {this.updateCheckbox(event.target.checked)}}
          />
        <button onClick={() => {this.submit()}}>Submit</button>
      </form>
    )
  }
}

ご覧のように、この新しいコンポーネントは他のもの以上にさらに多くのコンテンツを処理しています。初心者のために、このコンポーネントは3つの入力要素を定義しています(実際は、2つの input タグと1つの button ですが、このボタンも入力要素と考えられています)。ひとつめはユーザーが電子メールアドレスを入力できるようにします。ふたつめは任意の条件に同意するか否か、ユーザーが定義できるチェックボックスです。みっつめはユーザーが定期購買プロセスを終了するためにクリックするボタンです。

また、ご覧のように、このコンポーネントは acceptedTermsemail のふたつのフィールドで内部状態(this.state)を定義しています。この場合、このフォームは架空の条件に関連してユーザーの選択を表す acceptedTerms フィールドとユーザーの電子メールアドレスを保持する email フィールドを使用します。それから、ユーザーが提出ボタンをクリックすると、このフォームはその内部状態を使用して AJAX 要求 を発行します。

ですから、基本的に、ユーザー入力のように内部状態によって決まる動的なことを処理するコンポーネントが必要であれば、クラスコンポーネントが必要になります。しかし、内部状態によって決まるロジックを内部で実行しないコンポーネントが必要であれば、機能コンポーネントを使います。

注: これはコンポーネントの違いとその機能について簡単に説明したものです。実際、この章で作成する最後のコンポーネント SubscriptionForm は簡単に機能コンポーネントに変えることができます。この場合、その内部状態をコンポーネントツリーに移動し、状態の変更をトリガするためにその値や機能をパスする必要があります。React コンポーネントについての詳細は、この記事を確認してください

React コンポーネントを再レンダリングする

理解しなければならないもうひとつの重要な概念はどのようにして、いつ React がコンポーネントを再レンダリングするかということです。幸運なことに、この概念は簡単です。React コンポーネントで再レンダをトリガできるのは、コンポーネントが受信する props への変更、またはその内部状態への変更の2つだけです。

前章では、コンポーネントの内部状態をどのようにして変更するかについては説明しませんでしたが、どのようにして実行するかについては説明しました。ステートフルコンポーネント(クラスコンポーネントなど)を使用するときはいつでも setState メソッドを通してその状態を変更してその再レンダをトリガできます。重要なことは state フィールドを直接変更 しない ことを忘れないでください。次のように新しい必要な状態で setState メソッドを呼び出さなければ なりません

// this won't trigger a re-render:
updateCheckbox(checked) {
  this.state.acceptedTerms = checked;
}

// this will trigger a re-render:
this.setState({
  acceptedTerms: checked,
});

つまり、this.state を不変のように処理しなければなりません。

注: 高パフォーマンスを達成するために、React は setState()this.state を即座に更新することは保証しません。このライブラリは、さらに更新しなければならないより良い機会を待ちます。ですから、setState() を呼び出した直ぐ後に this.state を読み込むには安定しません。詳細については、setState() についての公式ドキュメントを確認してください。

では、ステートレスコンポーネント(機能コンポーネントなど)については、再レンダをトリガする唯一の方法はそれに送られた props を変更することです。前章では、機能コンポーネントがどのように使用され、本当は props は何かという全体的なコンテキストを見るチャンスがありませんでした。幸運なことに、これも簡単に理解できるトピックです。React では、props はコンポーネントにパスされたプロパティ以上のものではありません(その名の通り)。

ですから、前章で定義の UserProfile コンポーネントでパスされた/使用されたプロパティは userProfile だけです。ただし、前章では、このコンポーネントにパスしているプロパティ(props)を担当したものがありませんでした。この場合、そのコンポーネントをどこで、どのように使用するかがありませんでした。それをするには、以下のように HTML 要素のようにコンポーネントを使用する必要があります(これは JSX の便利な機能です)。

import React from 'react';
import UserProfile from './UserProfile';

class App extends () {
  constructor(props) {
    super(props);
    this.state = {
      user: {
        name: 'Bruno Krebs',
        picture: 'https://cdn.auth0.com/blog/profile-picture/bruno-krebs.png',
      },
    };
  }

  render() {
    return (
      <div>
        <UserProfile userProfile={this.state.user} />
      </div>
    );
  }
}

それだけです。これが props を定義し子コンポーネントにパスする方法です。では、親コンポーネント(App)の user を変更すると、これがコンポーネント全体で再レンダをトリガし、その後、props を変更して UserProfile にパスされてそれにも再レンダをトリガします。

注: React はその props が変更されればクラスコンポーネントも再レンダします。これは機能コンポーネントの特定動作ではありません。

React で何を構築するか

さあ、準備はいいですか?前章で説明した概念を念頭に、初めての React アプリケーションを作成する準備ができました。次の章では、ユーザーが互いに質疑応答をして対話できる簡単な Q&A アプリ(質疑応答)を作成します。このプロセス全体を現実的にするために、ラフなバックエンド API を作るために Node.js と Express を使用していきます。Node.js を使ったバックエンドアプリを作成する方法をご存知なくてもご心配なく。これは簡潔なプロセスで、たちまちの間に機能するようになるでしょう。

このチュートリアルの最後には、次のような Node.js バックエンドにサポートされたアプリができます。

React Tutorial: Example of final Q&A App

バックエンド API を Node.js と Express で作成する

React に進む前に、Q&A アプリをサポートするバックエンド API を素早く作成します。本章では、Node.js と一緒に Express を使ってこの API を作ります。Express とは何かやどのように機能するかを知らなくてもご心配なく。今回はその詳細に精通する必要はありません。公式なドキュメントで表明された Express は Node.js 用のオープンで軽量の Web フレームワークです。このライブラリは、後にご覧いただけるように、素早くアプリを作成してサーバー(バックエンドアプリなど)で実行できます。

ですから、始めるにあたり、端末のレーティングシステムを開き、プロジェクトを作成するディレクトリに移動し、次のコマンドを発行します。

# create a directory for your project
mkdir qa-app

# move into it
cd qa-app

# create a directory for your Express API
mkdir backend

# move into it
cd backend

# use NPM to start the project
npm init -y

最後のコマンドが backend ディレクトリ内に package.json と呼ばれるファイルを作成します。このファイルはバックエンド API の詳細(依存関係のように)を保持します。それから、これらコマンドの後、次を実行します。

npm i body-parser cors express helmet morgan

このコマンドはプロジェクトに次の5つの依存関係をインストールします。

  • body-parser:これは受信した要求の本文を JSON オブジェクトに変換するために使用するライブラリです。
  • cors:これは、Express を構成するために使用し、API がその他の配信元から受信する要求を承認することを表明するヘッダーを追加するライブラリです。これは CORS(クロス オリジン リソース共有) としても知られます。
  • express:これ自体が Express です。
  • helmet:これはさまざまな HTTP ヘッダーで Express アプリをセキュアにするのに役立つライブラリです。
  • morgan:これは Express アプリにログ機能を追加するライブラリです。

注: 本書の目標は初めての React アプリケーションの作成をお手伝いすることですので、上記のリストには各ライブラリが表に提示することの簡単な説明があります。その機能の詳細については、これらライブラリの公式な Web ページを参照してください。

これらライブラリをインストールした後、dependencies プロパティに含むためにNPM が package.json ファイルを変更したことが分かります。また、package-lock.json と呼ばれる新しいフィールドもあります。NPM はこのファイルを使ってプロジェクトを使う他の誰もが(または他の環境での自分自身さえも)現在インストールしているバージョンと互換性があるようにします。

それから、最後に実行する必要があるのはバックエンド ソースコードを作成することです。ですから、backend ディレクトリ内に src と呼ばれるディレクトリを作り、この新規ディレクトリ内に index.js と呼ばれるファイルを作ります。このファイルでは、次のコードを追加できます。

//import dependencies
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');

// define the Express app
const app = express();

// the database
const questions = [];

// enhance your app security with Helmet
app.use(helmet());

// use bodyParser to parse application/json content-type
app.use(bodyParser.json());

// enable all CORS requests
app.use(cors());

// log HTTP requests
app.use(morgan('combined'));

// retrieve all questions
app.get('/', (req, res) => {
  const qs = questions.map(q => ({
    id: q.id,
    title: q.title,
    description: q.description,
    answers: q.answers.length,
  }));
  res.send(qs);
});

// get a specific question
app.get('/:id', (req, res) => {
  const question = questions.filter(q => (q.id === parseInt(req.params.id)));
  if (question.length > 1) return res.status(500).send();
  if (question.length === 0) return res.status(404).send();
  res.send(question[0]);
});

// insert a new question
app.post('/', (req, res) => {
  const {title, description} = req.body;
  const newQuestion = {
    id: questions.length + 1,
    title,
    description,
    answers: [],
  };
  questions.push(newQuestion);
  res.status(200).send();
});

// insert a new answer to a question
app.post('/answer/:id', (req, res) => {
  const {answer} = req.body;

  const question = questions.filter(q => (q.id === parseInt(req.params.id)));
  if (question.length > 1) return res.status(500).send();
  if (question.length === 0) return res.status(404).send();

  question[0].answers.push({
    answer,
  });

  res.status(200).send();
});

// start the server
app.listen(8081, () => {
  console.log('listening on port 8081');
});

以下のリストがこのファイルでどのように機能するかについて簡単に説明しています(また、上記のコード内のコメントも確認してください)。

  • すべてが5つの require ステートメントで始まる。これらステートメントは NPM でインストールしたすべてのライブラリをロードする。
  • その後、Express を使って新規アプリ(const app = express();)を定義する。
  • それから、データベースとして作動する配列を作成する(const questions = [];)。実在のアプリでは、Mongo、PostgreSQL、MySQL などのような実際のデータベースを使用する。
  • 次に、Express アプリの use メソッドを4回呼び出す。それぞれが Express と一緒にインストールした異なるライブラリを構成する。
  • その直後、最初のエンドポイントを定義する(app.get('/', ...);)。このエンドポイントは質問のリストをそれを要求した者に送り返す担当をする。唯一注意するのは answers を送信する代わりに、このエンドポイントはこれらを一緒にコンパイルして各質問が持つ回答数のみを送信する。この情報は React アプリで使用する。
  • 最初のエンドポイントの後、別のエンドポイントを定義する。この場合、この新規エンドポイントは一つの質問との要求への応答を担当する(すべての回答と一緒に)。
  • このエンドポイントの後、第3のエンドポイントを定義する。ここでは、誰かが POST HTTP 要求を API に送信するとアクティブ化されるエンドポイントを定義する。ここでの目標は要求の body に送信されたメッセージをデータベースに newQuestion を挿入することである。
  • それから、API に最後のエンドポイントがある。このエンドポイントは回答を特定の質問に挿入する担当である。この場合、どの質問を新規回答に加えなければならないかを特定するために id と呼ばれるルートのパラメーターを使用します。
  • 最後に、Express アプリで listen 機能を呼び出し API バックエンドを実行します。

このファイルが整ったので、準備ができました。アプリを実行するには、次のコマンドを発行します。

# from the qa-app directory
node src

それから、すべてが機能しているかを確認するために、新しく端末を開き、次のコマンドを発行します。

# issue an HTTP GET request
curl localhost:8081

# issue a POST request
curl -X POST -H 'Content-Type: application/json' -d '{
  "title": "How do I make a sandwich?",
  "description": "I am trying very hard, but I do not know how to make a delicious sandwich. Can someone help me?"
}' localhost:8081

curl -X POST -H 'Content-Type: application/json' -d '{
  "title": "What is React?",
  "description": "I have been hearing a lot about React. What is it?"
}' localhost:8081

# re-issue the GET request
curl localhost:8081

ご存知ない方のために、curl は簡単に HTTP 要求を発行するコマンド ライン インターフェイスです。上記のコード スニペットでは、ご覧のように、わずか数個のキーストライクで、異なるタイプの HTTP 要求を発行し、必要なヘッダー(-H)を定義し、データ(-d)をバックエンドに送ります。

最初のコマンドは印刷された空の配列([])になる HTTP GET 要求をトリガします。それから、第2と第3のコマンドは2つの質問を API に挿入する POST 要求を発行し、第4のコマンドはこれら質問が適切に挿入されているかを確認する別の GET 要求を発行します。

想定の結果を得ることができたら、サーバーを実行したままにし、次の章に進みます。

React でアプリケーションを作成する

バックエンド API を実行したままにしたら、ついに React アプリケーションを作成する用意ができました。それほど昔ではありませんが、React でアプリを作成しようとする開発者は React アプリケーションをスキャフォールディングするために必要なすべてのツール(webpack など)を設定するのが大変でした。しかし、(そして幸運なことに)、この状況は Facebook が React アプリを作成すると呼ばれるツールを発行した後、変わりました。

このツールで、ひとつのコマンドで新規 React アプリケーションをスキャフォールディングできます。同様に、React アプリを作成するには、新しく端末を開き、backend Node.js アプリを作成した同じディレクトリに移動します(qa-app ディレクトリから)。そこから、次のコマンドを発行します。

# the npx command was introduced on npm@5.2.0
npx create-react-app frontend

これで NPM をダウンロードし、1度のコマンドで create-react-app を実行し、新規アプリケーションの必要なディレクトリとして frontend に送ります。新規アプリケーションのスキャフォールディングに関するプロセスは上記のコマンドを実行した後にご覧のように、あまり簡単ではありません。このツールは全体を作るたくさんの秒数(またはインターネット接続によって数分)が必要です。ただし、このツールが終了したら、次のコマンドを発行して React アプリを実行します。

# move into the new directory
cd frontend

# start your React app
npm start

注: Yarn をインストールしたら、create-react-app ツールはプロジェクトをBootstrapするためにそれを使用します。同様に、yarn start を使用するか、npm start を実行する前に npm install を実行しなければなりません。

上記で発行した最後のコマンドはポート 3000 でリッスンする開発サーバーを開始し、デフォルトの Web ブラウザーの新規アプリを開きます。

Welcome to React local starting page

アプリが見えたら、Ctrl + C をクリックしてサーバーを停止し、アプリケーションで必要な数個の依存関係をインストールできるようにします。そこで、端末に戻って、サーバーを停止したら、次のコマンドを実行します。

npm i react-router react-router-dom

このコマンドはアプリでナビゲーションの処理に役立つ2つのライブラリをインストールします。最初の react-router はシームレスなナビゲーションを可能にするメインのライブラリです。ふたつめは react-router-dom で、React ルーターの DOM 結合を提供します。

注: モバイルデバイス用のアプリを作成するために React ネイティブを使っているのであれば、代わりに react-router-native をインストールします。

それから、これらライブラリをインストールした後、優先する IDE で React プロジェクトを開き、実際の作業ができるようにします。

React アプリをクリーニングする

アプリを作成する前に、ファイルを削除してコードのクリーニングをします。初心者の方はこのチュートリアルでは自動テストを作成しませんので ./src/App.test.js ファイルを削除します。これは重要なトピックですが、ここではこれを飛ばして React の学習に重点を置きます。

注: React について学んだ後、自動テストをアプリに追加する方法について学ぶのも良いでしょう。その方法については React アプリケーションを Jest でテストする ブログ投稿をご覧ください。

そのほかに、./src/logo.svg./src/App.css の2つのファイルも使いませんので、削除できます。それから、これらファイルを削除した後、./src/App.js ファイルを開き、そのコードと次を置き換えます。

import React, { Component } from 'react';

class App extends Component {
  render() {
    return (
      <div>
        <p>Work in progress.</p>
      </div>
    );
  }
}

export default App;

App コンポーネントの新バージョンはもうすぐこのファイルのコンテンツと置き換えるので、あまり使いません。しかし、コンパイルしないコードがあることを避けるために、App コンポーネントをリファクターすることをお勧めします。

React ルーターをアプリに構成する

クリーニングしたら、React ルーターをアプリに構成する必要があります。これはご覧のように、とても簡単なステップです。ただし、React ルーターをマスターするには、具体的に本件とその全機能を紹介する 少なくても もうひとつの記事を読む必要があります。

React ルーターは非常に完全なソリューションで、初めての React アプリでは氷山の一角に触れるだけです。React ルーターについて学ぶ方は、公式ドキュメントをご覧ください

「React ルーターは素晴らしいアプリケーションを作るのに役立つ強力なソリューションです。」

この点を念頭に、./src/index.js ファイルを開き、そのコンテンツと次を置き換えます。

import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

このファイルの新規バージョンでは、react-router-dom ライブラリから BrowserRouter をインポートし、App コンポーネントをこのルーター内にカプセル化します。React ルーターを使い始めるために必要なのはこれだけです。

注: このファイルを以前見たことがなければ、これは React アプリを表示するロジックの一部です。さらに具体的には、document.getElementById('root') は HTML 要素 React がアプリをレンダーしなければならないものを定義します。この root 要素は ./public/index.html ファイル内にあります。

React アプリに Bootstrap を構成する

React アプリをユーザー インターフェイス の観点から作成するために、その上に Bootstrap を構成します。Bootstrapを知らない方のために、これは開発者がルックスが良く、反応が早い Web アプリを簡単に作るのに役立つ非常に人気の高いライブライです。

React とBootstrapを一緒に統合するには複数の方法があります。しかし、初めてのアプリケーションの要件はとてもシンプルで、その対話型コンポーネントは必要ないので(ユーザーはこのライブラリが提供する基本的なスタイルに興味があるなど)、最も簡単な戦略に従っていきます。つまり、./public/index.html ファイルを開き、それを次のようにアップデートします。

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... tags above the title stay untouched ... -->
    <title>Q&App</title>
    <link rel="stylesheet" href="https://bootswatch.com/4/flatly/bootstrap.min.css">
  </head>
  <!-- ... body definition stays untouched ... -->
</html>

この場合、 React アプリの titleQ&App に変え、flatly と呼ばれるBootstrapのバリエーションをロードするアプリを作るという2つのことを実行します。ご興味があれば Bootswatch で使用可能なバリエーションを使うか、あるいはBootstrapのデフォルトフレーバーを使うことができます。ただし、おそらく Bootswatch で使用可能なさらに魅力的なバリエーションを見つけられるでしょう。

React アプリにナビゲーションバーを作る

これでBootstrapを使用するアプリを作ったので、最初の React コンポーネントを作る用意ができました。本章では、NavBar(ナビゲーションバーを表す)と呼ばれるコンポーネントを作り、React アプリにそれを追加します。

このためには、アプリケーションのディレクトリ src 内に NavBar と呼ばれる新しいディレクトリを作り、それの中に NavBar.js と呼ばれる新しいファイルを挿入します。この中に次のコードを入力します。

import React from 'react';
import {Link} from 'react-router-dom';

function NavBar() {
  return (
    <nav className="navbar navbar-dark bg-primary fixed-top">
      <Link className="navbar-brand" to="/">
        Q&App
      </Link>
    </nav>
  );
}

export default NavBar;

ご覧のように、作成中のナビゲーションバーのコンポーネントは機能コンポーネントです。内部状態を保持する必要はありませんから、ステートレスな(例:機能)コンポーネントのように作成できます。

では、新規コンポーネントを使用するには、./src/App.js ファイルを開き、次のように更新できます。

import React, { Component } from 'react';
import NavBar from './NavBar/NavBar';

class App extends Component {
  render() {
    return (
      <div>
        <NavBar/>
        <p>Work in progress.</p>
      </div>
    );
  }
}

export default App;

それから、端末から npm start を発行してアプリを実行すれば、その上部にナビゲーションバーが表示されます。ただし、App コンポーネントに含まれる「進行中の作業」メッセージは表示されません。ここでの問題は作成したナビゲーションバーは上部に固定させるBootstrapが提供する CSS クラス(fixed-top)を使うことです。つまり、このコンポーネントは通常の div 要素であるかのように既定の上下空間を取りません。

この状況を修正するには、./src/index.css ファイルを開き、以下で表示のように margin-top ルールを追加します。

body {
  /* ... other rules ... */
  margin-top: 100px;
}

/* ... other rules ... */

ここで、アプリを再度チェックすると、ナビゲーションバーと「進行中の作業」メッセージが表示されます。

React application with Bootstrap navigation bar

React でクラスコンポーネントを作成する

ナビゲーションバーを作成した後、次にやることはバックエンドから質問をフェッチするステートフルコンポーネント(クラスコンポーネント)を作り、ユーザーにそれを表示することです。これら質問をフェッチするには、もうひとつのライブラリ Axios の助けが必要です。簡単に言うと Axios はブラウザー用や Node.js 用のプロミスベースの HTTP クライアントです。本チュートリアルでは、ブラウザーのみで(React アプリなどで)それを使用します。

Axios をインストールするには、React 開発サーバーを停止し、次のコマンドを発行します。

npm i axios

それから、src 内に Questions と呼ばれる新規ディレクトリを、その中に Questions.js と呼ばれる新規ファイルを作ります。このファイルでは、次のコードを挿入できます。

import React, {Component} from 'react';
import {Link} from 'react-router-dom';
import axios from 'axios';

class Questions extends Component {
  constructor(props) {
    super(props);

    this.state = {
      questions: null,
    };
  }

  async componentDidMount() {
    const questions = (await axios.get('http://localhost:8081/')).data;
    this.setState({
      questions,
    });
  }

  render() {
    return (
      <div className="container">
        <div className="row">
          {this.state.questions === null && <p>Loading questions...</p>}
          {
            this.state.questions && this.state.questions.map(question => (
              <div key={question.id} className="col-sm-12 col-md-4 col-lg-3">
                <Link to={`/question/${question.id}`}>
                  <div className="card text-white bg-success mb-3">
                    <div className="card-header">Answers: {question.answers}</div>
                    <div className="card-body">
                      <h4 className="card-title">{question.title}</h4>
                      <p className="card-text">{question.description}</p>
                    </div>
                  </div>
                </Link>
              </div>
            ))
          }
        </div>
      </div>
    )
  }
}

export default Questions;

このファイルでは、いくつかの重要なことがあります。まず、上記で述べたように、API バックエンドで使用可能な質問を保持するステートフルコンポーネントを作成します。ですから、これを正しく行うには、null に設定した questions プロパティでコンポーネントを始め、React がコンポーネント(componentDidMount メソッド)をマウントし終えたとき、GET リクエスト(axios.get コールを通して)をバックエンドに発行します。要求とバックエンドからの応答の間、React は「質問の読み込み中」というメッセージでコンポーネントをレンダーします。(this.state.questions === null && をメッセージの前に追加してそのように動作するよう指示したので、そのように実行されます。)

注: このコンポーネントは本書 React コンポーネントのライフサイクルで取り扱われていないトピックに触れています。この場合、React によって提供される拡張ポイントのひとつ componentDidMount メソッドを使用します。このチュートリアルに従うために、この機能をよく理解する必要はありませんが、これを終えた後、このトピックについて学ぶことをお勧めします。

それから、Axios がバックエンドから応答を得たら、questions と呼ばれる定数内に data を戻し、それでコンポーネント(this.setState)の状態を更新します。上記で学んだように、この更新はリレンダーをトリガーし、取得したすべての質問を React が表示します。

では、質問がどのように表示されるかに関しては、素晴らしい カードコンポーネントを作るBootstrapによって提供された CSS クラスで多数の div 要素を使います。このカードがどのように表示されるかを調節するときは、必ずそのドキュメントをチェックしてください。

そのほかに、/question/${question.id} をクリックしたときに、次のパスにユーザーをリダイレクトさせる Link と呼ばれる(react-router-dom から)コンポーネントを使うことにご留意ください。次の章では、ユーザーが選択した質問に対する回答を表示するコンポーネントを作成していきます。

では、コンポーネントの動作について学びましたので、次に行うことは App コンポーネントのコードを更新して新規コンポーネントを使用します。

import React, { Component } from 'react';
import NavBar from './NavBar/NavBar';
import Questions from './Questions/Questions';

class App extends Component {
  render() {
    return (
      <div>
        <NavBar/>
        <Questions/>
      </div>
    );
  }
}

export default App;

では、アプリを再度実行すると(npm start)、次のようなページが表示されます。

React app using Axios to fetch data from a backend API.

React Router でユーザーをルーティングする

これら機能を設定したので、学ばなければならない重要なステップは React アプリでルーティングをどのように処理するかについてです。本章では、バックエンドで使用可能な質問の詳細を表示するコンポーネントを作成しながら、このトピックについて学んでいきましょう。

初心者用に、Question(単数形)と呼ばれる新規ディレクトリを作り、その中に Question.js (これも単数形)と呼ばれるファイルを作ります。それから、このファイルに次のコードを挿入します。

import React, {Component} from 'react';
import axios from 'axios';

class Question extends Component {
  constructor(props) {
    super(props);
    this.state = {
      question: null,
    };
  }

  async componentDidMount() {
    const { match: { params } } = this.props;
    const question = (await axios.get(`http://localhost:8081/${params.questionId}`)).data;
    this.setState({
      question,
    });
  }

  render() {
    const {question} = this.state;
    if (question === null) return <p>Loading ...</p>;
    return (
      <div className="container">
        <div className="row">
          <div className="jumbotron col-12">
            <h1 className="display-3">{question.title}</h1>
            <p className="lead">{question.description}</p>
            <hr className="my-4" />
            <p>Answers:</p>
            {
              question.answers.map((answer, idx) => (
                <p className="lead" key={idx}>{answer.answer}</p>
              ))
            }
          </div>
        </div>
      </div>
    )
  }
}

export default Question;

この新規コンポーネントの機能は Questions コンポーネントの機能と非常によく似ています。これは Axios を使用するステートフルコンポーネントで、質問の詳細全体を取得するエンドポイントに GET 要求を発行し、応答を得たときにページを更新します。

ここで新しいものは何もありませんが、このコンポーネントがレンダーされる方法だけが新しいことです。

では App.js ファイルを開き、そのコンテンツと次のコンテンツとを置き換えます。

import React, { Component } from 'react';
import {Route} from 'react-router-dom';
import NavBar from './NavBar/NavBar';
import Question from './Question/Question';
import Questions from './Questions/Questions';

class App extends Component {
  render() {
    return (
      <div>
        <NavBar/>
        <Route exact path='/' component={Questions}/>
        <Route exact path='/question/:questionId' component={Question}/>
      </div>
    );
  }
}

export default App;

App コンポーネントの新規バージョンでは、2つの Route 要素(react-router-dom が提供する)を使い、レンダーされた Questions コンポーネントが必要なときや、レンダーされた Question コンポーネントが必要なときに React に伝えます。さらに具体的には、ユーザーが /(exact path='/')に移動するときに Questions が表示され、ユーザーが /question/:questionId に移動するときに特定の質問の詳細が表示されるよう React に示します。

最後のルートは questionId と呼ばれるパラメーターを定義することにご留意ください。Questions(複数形)コンポーネントを作成するとき、その質問の id を使用するリンクを加えます。React Router はこの id を使ってリンクを作り、それを Question コンポーネント(params.questionId)に与えます。この id でこのコンポーネントは Axios を使用し、正確にどの質問が要求されているかをバックエンドに伝えます。

ここで、アプリケーションをチェックすれば、ホームページにすべての質問が表示されているので、特定の質問に移動することができます。ただし、回答をまだ加えていないので、新規コンポーネントで回答を見ることはできません。現時点では、質問に回答を加えるには、以下と同様の要求を発行します。

curl -X POST -H 'Content-Type: application/json' -d '{
  "answer": "Just spread butter on the bread, and that is it."
}' localhost:8081/answer/1

その後、アプリをリロードして http://localhost:3000/question/1 に移動すると、次のようなページが表示されます。

React Q&A app Question page configured with React Router

React アプリをセキュアにする

このアプリケーションはプライムタイムに必要なほとんどすべてがそろった状態になりました。いくつかの機能がないだけです。例えば、この時点では、ユーザーがアプリを通して質問を作成したりそれらに答えたりする手段がありません。もうひとつの例は、アプリケーションにログインする方法がありません。そのほかに、この質問と回答はその作成者についての情報を提供しません。

本章では、これら機能を簡単に実装する方法について学んでいきます。まず、認証機能が役立つように Auth0 に加入します。それから、バックエンドをセキュアにします。最後に、React アプリをセキュアにし Question コンポーネントをリファクターして、認証されたユーザーが質問に回答できるようにします。

Auth0 アカウントを構成する

初心者は、アプリケーションに統合できるようにまず Auth0 にサインアップします。すでに既存のアカウントがある場合は、問題なくそれを使用できます。まだアカウントがない方は、無料 Auth0 アカウントにサインアップする良い機会です。無料アカウントで、次の機能にアクセスできます。

サインアップした後、Auth0 アプリケーションを作成してアプリを表します。そこで、ダッシュボードで、垂直方向メニューにある アプリケーション セクションをクリックしてから、アプリケーションの作成 をクリックします。

以下のダイアログで、アプリケーションの名前を挿入し、(例:「Q&App」)、それからそのタイプとしてシングルページ アプリケーション を選択します。それから。作成 ボタンをクリックすると、Auth0 はアプリケーションを作成し、クイック スタート セクションにリダイレクトします。そこから、設定 タブをクリックして Auth0 アプリケーションの構成を変更し、そこから値をコピーします。

Creating a React application on Auth0.

そこで、設定 タブに移動して、許可されたコールバック URL フィールドを検索し、それに http://localhost:3000/callback を挿入します。

この URL の意味が何で、なぜそれが必要なのか疑問に思われていると思います。この URL が必要な理由は、Auth0 を通して認証しながら、ユーザーはその Universal Login Page(汎用ログインページ)にリダイレクトされ、認証プロセスの後(成功か否かにかかわらず)、アプリケーションにリダイレクトされます。セキュリティ上の理由のため、Auth0 はこのフィールドに登録されている URL のみにユーザーをリダイレクトされます。

この値を設定し、変更を保存 ボタンをクリックし、このページを開いたままにします。

Auth0 で API バックエンドをセキュアにする

Auth0 で Node.js API をセキュアにするには、次の2つのライブラリをインストールし構成します。

  • express-jwt:JSON Webトークン(JWT)を確認し、その属性で req.user を設定するミドルウェア。
  • jwks-rsa:JWKS (JSON Web Key Set)エンドポイントから RSA 公開キーを取得するライブラリ。

これらライブラリをインストールするには、Ctrl + C を押して API バックエンドを停止し、次のコマンドに従います。

# from the backend directory
npm i express-jwt jwks-rsa

その後、./src/index.js ファイルを開き、これらライブラリを次のようにインポートします。

// ... other require statements ...
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');

それから、このファイル上に、最初の POST エンドポイント(app.post)の直前に次の定数を作ります。

// ... require statements ...

// ... app definitions ...

// ... app.get endpoints ...

const checkJwt = jwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `https://<YOUR_AUTH0_DOMAIN>/.well-known/jwks.json`
  }),

  // Validate the audience and the issuer.
  audience: '<YOUR_AUTH0_CLIENT_ID>',
  issuer: `https://<YOUR_AUTH0_DOMAIN>/`,
  algorithms: ['RS256']
});

// ... app.post endpoints ...

// ... app.listen ...

この定数は実際、ID トークンを検証する Express ミドルウェアです。それが機能するには <YOUR_AUTH0_CLIENT_ID> プレースホルダーと Auth0 アプリケーションの クライアント ID フィールドで提示される値とを置き換えなければなりません。また、<YOUR_AUTH0_DOMAIN>ドメイン フィールド(例: bk-tmp.auth0.com)で提示される値も置き換えなければなりません。

それから、checkJwt ミドルウェアを使用する2つの POST エンドポイントを作る必要があります。これをするには、これらエンドポイントと次を置き換えます。

// insert a new question
app.post('/', checkJwt, (req, res) => {
  const {title, description} = req.body;
  const newQuestion = {
    id: questions.length + 1,
    title,
    description,
    answers: [],
    author: req.user.name,
  };
  questions.push(newQuestion);
  res.status(200).send();
});

// insert a new answer to a question
app.post('/answer/:id', checkJwt, (req, res) => {
  const {answer} = req.body;

  const question = questions.filter(q => (q.id === parseInt(req.params.id)));
  if (question.length > 1) return res.status(500).send();
  if (question.length === 0) return res.status(404).send();

  question[0].answers.push({
    answer,
    author: req.user.name,
  });

  res.status(200).send();
});

両方のエンドポイントは2つの変更のみを導入します。まず、両方とも checkJwt を使用することを宣言しており、非認証ユーザーの使用をできなくします。ふたつめは、両方とも author と呼ばれる新しいプロパティを質問と回答に追加します。これら新しいプロパティは要求を発行するユーザーの名前(req.user.name)を受けます。

これら変更を設定すると、バックエンド API を再度開始でき(node src)、React アプリケーションのリファクタリングを始めることができます。

注: 公的なアクセス可能にするため、checkJwt ミドルウェアは GET エンドポイントに追加しません。つまり、非認証ユーザーは質問と回答を見ることができますが、新しい質問を作ったり既存の質問に回答したりできなくすることです。

注2: バックエンド API はメモリのデータのみを保有するので、再起動すると依存に挿入した質問と回答すべてを失います。curl を通して新しい質問を追加するには、Auth0 から ID トークンをフェッチしなければなりません。ただし、アプリのインターフェイスを通して質問と回答を加えるアプリ全体が終了するまで待つことができます。

React アプリを Auth0 でセキュアにする

React アプリケーションを Auth0 でセキュアにするには、ひとつのライブラリ auth0-js のみをインストールしなければなりません。これはユーザーが使っているような SPA をセキュアにする Auth0 が提供する公式ライブラリです。これをインストールするには、開発サーバーを停止し、次のコマンドを発行します。

# from the frontend directory
npm install auth0-js

その後、認証ワークフローで役に立つクラスを作ります。そのためには src ディレクトリ内に Auth.js と呼ばれる新しいファイルを作り、次のコードを挿入します。

import auth0 from 'auth0-js';

class Auth {
  constructor() {
    this.auth0 = new auth0.WebAuth({
      // the following three lines MUST be updated
      domain: '<YOUR_AUTH0_DOMAIN>',
      audience: 'https://<YOUR_AUTH0_DOMAIN>/userinfo',
      clientID: '<YOUR_AUTH0_CLIENT_ID>',
      redirectUri: 'http://localhost:3000/callback',
      responseType: 'id_token',
      scope: 'openid profile'
    });

    this.getProfile = this.getProfile.bind(this);
    this.handleAuthentication = this.handleAuthentication.bind(this);
    this.isAuthenticated = this.isAuthenticated.bind(this);
    this.signIn = this.signIn.bind(this);
    this.signOut = this.signOut.bind(this);
  }

  getProfile() {
    return this.profile;
  }

  getIdToken() {
    return this.idToken;
  }

  isAuthenticated() {
    return new Date().getTime() < this.expiresAt;
  }

  signIn() {
    this.auth0.authorize();
  }

  handleAuthentication() {
    return new Promise((resolve, reject) => {
      this.auth0.parseHash((err, authResult) => {
        if (err) return reject(err);
        if (!authResult || !authResult.idToken) {
          return reject(err);
        }
        this.idToken = authResult.idToken;
        this.profile = authResult.idTokenPayload;
        // set the time that the id token will expire at
        this.expiresAt = authResult.idTokenPayload.exp * 1000;
        resolve();
      });
    })
  }

  signOut() {
    // clear id token, profile, and expiration
    this.idToken = null;
    this.profile = null;
    this.expiresAt = null;
  }
}

const auth0Client = new Auth();

export default auth0Client;

注: 上記のように <YOUR_AUTH0_CLIENT_ID><YOUR_AUTH0_DOMAIN> を Auth0 アプリケーションから抽出された値に置き換えなければなりません。

ご覧のように、このファイルでは、Auth クラスを定義するモジュールを次の7つの方法で作ります。

  • constructor:ここでは、auth0.WebAuth のインスタンスを Auth0 値で作り、その他重要な構成を定義します。例えば、Auth0 がユーザー(redirectUri)を http://localhost:3000/callback URL(上記で、許可されたコールバック URL フィールドに挿入したものと同じもの)にリダイレクトするように定義します。
  • getProfile:このメソッドはもしあれば、認証ユーザーのプロファイルを返します。
  • getIdToken:このメソッドは現在のユーザーのために Auth0 で生成された idToken を返します。これは POST エンドポイントに要求を発行中に使用します。
  • handleAuthentication:これは、ユーザーが Auth0 からリダイレクトされた直後にアプリが呼び出すメソッドです。このメソッドはユーザーの詳細や id トークンをフェッチする URL のハッシュセグメントを読み取ります。
  • isAuthenticated:このメソッドは認証ユーザーか否かを返します。
  • signIn:このメソッドは認証プロセスを初期化します。つまり、このメソッドは Auth0 ログインページにユーザーを送ります。
  • signOut:このメソッドは profileid_token、および expiresAtnull に設定してユーザーをサインアウトします。

最後に、このモジュールは Auth クラスのインスタンスを作り、それを世界中に公開します。つまり、このアプリでは、Auth クラスのインスタンスがひとつ以上ないということです。

このヘルパークラスを定義した後、ユーザーが認証できるように NavBar コンポーネントをリファクターできます。ですから NavBar.js ファイルを開き、そのコードと次のコードとを置き換えます。

import React from 'react';
import {Link, withRouter} from 'react-router-dom';
import auth0Client from '../Auth';

function NavBar(props) {
  const signOut = () => {
    auth0Client.signOut();
    props.history.replace('/');
  };

  return (
    <nav className="navbar navbar-dark bg-primary fixed-top">
      <Link className="navbar-brand" to="/">
        Q&App
      </Link>
      {
        !auth0Client.isAuthenticated() &&
        <button className="btn btn-dark" onClick={auth0Client.signIn}>Sign In</button>
      }
      {
        auth0Client.isAuthenticated() &&
        <div>
          <label className="mr-2 text-white">{auth0Client.getProfile().name}</label>
          <button className="btn btn-dark" onClick={() => {signOut()}}>Sign Out</button>
        </div>
      }
    </nav>
  );
}

export default withRouter(NavBar);

ナビゲーション バー コンポーネントの新しいバージョンは次の2つの新しい要素をインポートします。

  • withRouter:これはナビゲーション機能でコンポーネントを強化する React Router が提供するコンポーネントです(history オブジェクトにアクセスするなど)。
  • auth0Client:これは定義したばかりの Auth クラスのシングルトン インスタンスです。

auth0Client インスタンスで、NavBarサインイン ボタン(非認証ユーザー用)か、サインアウト ボタン(認証ユーザー用)をレンダーする必要があるかを決めます。ユーザーが正しく認証されれば、このコンポーネントはその名前も表示します。認証ユーザーが サインアウト ボタンを押すと、コンポーネントは auth0ClientsignOut メソッドを呼び出し、ユーザーをホームページにリダイレクトします。

NavBar コンポーネントをリファクタリングした後、コールバックルート(http://localhost:3000/callback)を処理するコンポーネントを作らなければなりません。このコンポーネントを定義するには、src ディレクトリ内に Callback.js と呼ばれる新しいファイルを作り、それに次のコードを挿入します。

import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';
import auth0Client from './Auth';

class Callback extends Component {
  async componentDidMount() {
    await auth0Client.handleAuthentication();
    this.props.history.replace('/');
  }

  render() {
    return (
      <p>Loading profile...</p>
    );
  }
}

export default withRouter(Callback);

定義したばかりのコンポーネントは2つを担当します。まず、Auth0 が送信したユーザー情報をフェッチする handleAuthentication メソッドを呼び出します。2つめは handleAuthentication プロセスを終えた後、ユーザーをホームページ(history.replace('/'))にリダイレクトします。一方、このコンポーネントは「プロファイルのロード中」というメッセ―ジを表示します。

それから、Auth0 で統合を終えるには、App.js ファイルを開き、それを次のように更新します。

// ... other import statements ...
import Callback from './Callback';

class App extends Component {
  render() {
    return (
      <div>
        <!-- ... NavBar and the other two Routes ... -->
        <Route exact path='/callback' component={Callback}/>
      </div>
    );
  }
}

export default App;

ここで、この React アプリを再度実行すると(npm start)、Auth0 を通して自分で認証できます。認証プロセスの後、ナビゲーションバー上にあなたの名前が表示されます。

React Q&A app user authentication with Auth0

認証ユーザーに機能を追加する

これで Auth0 の React アプリケーションへの統合が終わったので、認証ユーザーだけがアクセスできる機能を追加していきます。このチュートリアルを終えるには、2つの機能を実装します。まず、認証ユーザーが新しい質問を作れるようにします。それから、認証ユーザーがこれら質問に回答できるようにフォームを表示する Question(単数形)コンポーネントをリファクターします。

最初の機能には、アプリケーションに新しいルート /new-question を作ります。このルートは、ユーザーが認証されているか否かをチェックするコンポーネントで保護されています。そのユーザーがまだ認証されていなければ、このコンポーネントは認証されるようにユーザーを Auth0 にリダイレクトします。そのユーザーがすでに認証されていれば、このコンポーネントは新しい質問が作成されるフォームを React がレンダーします。

ですから、初心者の方は SecuredRoute と呼ばれる新しいディレクトリを作成し、その中に SecuredRoute.js と呼ばれるファイルを作ります。それから、このファイルで次のコードを挿入します。

import React from 'react';
import {Route} from 'react-router-dom';
import auth0Client from '../Auth';

function SecuredRoute(props) {
  const {component: Component, path} = props;
  return (
    <Route path={path} render={() => {
        if (!auth0Client.isAuthenticated()) {
          auth0Client.signIn();
          return <div></div>;
        }
        return <Component />
    }} />
  );
}

export default SecuredRoute;

このコンポーネントの目標は構成するルートへのアクセスを規制することです。これの実装はとても簡単です。この場合、ユーザーが認証されている場合にレンダーできるように Component と、React Router が提供する既定 Route コンポーネントを構成できるように path の2つのプロパティが必要な機能コンポーネントを作ります。ただし、レンダリングする前に、このコンポーネントはユーザーが isAuthenticated であるかをチェックします。ユーザーが認証されていなければ、このコンポーネントはユーザーをログインページにリダイレクトする signIn メソッドをトリガーします。

それから、SecuredRoute コンポーネントを作成した後、ユーザーが質問を作成するフォームをレンダーするコンポーネントを作成できます。そのためには、NewQuestion と呼ばれる新しいディレクトリを作り、NewQuestion.js と呼ばれるファイルをその中に作成します。それから、このコードをそのファイルに挿入します。

import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';
import auth0Client from '../Auth';
import axios from 'axios';

class NewQuestion extends Component {
  constructor(props) {
    super(props);

    this.state = {
      disabled: false,
      title: '',
      description: '',
    };
  }

  updateDescription(value) {
    this.setState({
      description: value,
    });
  }

  updateTitle(value) {
    this.setState({
      title: value,
    });
  }

  async submit() {
    this.setState({
      disabled: true,
    });

    await axios.post('http://localhost:8081', {
      title: this.state.title,
      description: this.state.description,
    }, {
      headers: { 'Authorization': `Bearer ${auth0Client.getIdToken()}` }
    });

    this.props.history.push('/');
  }

  render() {
    return (
      <div className="container">
        <div className="row">
          <div className="col-12">
            <div className="card border-primary">
              <div className="card-header">New Question</div>
              <div className="card-body text-left">
                <div className="form-group">
                  <label htmlFor="exampleInputEmail1">Title:</label>
                  <input
                    disabled={this.state.disabled}
                    type="text"
                    onBlur={(e) => {this.updateTitle(e.target.value)}}
                    className="form-control"
                    placeholder="Give your question a title."
                  />
                </div>
                <div className="form-group">
                  <label htmlFor="exampleInputEmail1">Description:</label>
                  <input
                    disabled={this.state.disabled}
                    type="text"
                    onBlur={(e) => {this.updateDescription(e.target.value)}}
                    className="form-control"
                    placeholder="Give more context to your question."
                  />
                </div>
                <button
                  disabled={this.state.disabled}
                  className="btn btn-primary"
                  onClick={() => {this.submit()}}>
                  Submit
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

export default withRouter(NewQuestion);

このコンポーネントのコードは長いですが、複雑ではありません。ご覧のように、この場合、次の状態を保持できるようにクラスコンポーネントを作成する必要があります。

  • disabled:これを使って、ユーザーが 提出 ボタンを押した後に入力要素を無効にします。
  • title:これを使って、ユーザーが尋ねられた質問のタイトルを定義できるようにします。
  • description:これを使って、ユーザーが質問の詳細を定義できるようにします。

また、constructorrender の他に次の3つの必要なメソッドが表示されます。

  • updateDescription:このメソッドはコンポーネントの状態上の description を更新する担当です。
  • updateTitle:このメソッドはコンポーネントの状態上の description を更新する担当です。
  • submit:このメソッドはバックエンドに新しい質問を発行し、要求されている間に入力フィールドをブロックする担当です。

Submit メソッドでは、要求にそれを加えるために現在のユーザーの ID トークンを得る auth0Client を使っています。このトークンなしでは、バックエンド API は要求を拒否します。

UI の観点から、このコンポーネントは素晴らしいフォームを作るたくさんの Bootstrap クラスを使用しています。Bootstrap でのフォームについて学ぶ必要がある場合は、このチュートリアルを終えた後、必ずこのリソースをチェックしてください。

では、この機能を確認するために、2つのファイルを更新します。まず、App.js ファイルで新しいルートを登録します。

// ... other import statements ...
import NewQuestion from './NewQuestion/NewQuestion';
import SecuredRoute from './SecuredRoute/SecuredRoute';

class App extends Component {
  render() {
    return (
      <div>
        <!-- ... navbar and other routes ... -->
        <SecuredRoute path='/new-question' component={NewQuestion} />
      </div>
    );
  }
}

export default App;

それから、Questions.js ファイルのリンクをこの新しいルートに加えます。このためには、このファイルを開き、次のように更新します。

// ... import statements ...

class Questions extends Component {
  // ... constructor and componentDidMount ...

  render() {
    return (
      <div className="container">
        <div className="row">
          <Link to="/new-question">
            <div className="card text-white bg-secondary mb-3">
              <div className="card-header">Need help? Ask here!</div>
              <div className="card-body">
                <h4 className="card-title">+ New Question</h4>
                <p className="card-text">Don't worry. Help is on the way!</p>
              </div>
            </div>
          </Link>
          <!-- ... loading questions message ... -->
          <!-- ... questions' cards ... -->
        </div>
      </div>
    )
  }
}

export default Questions;

これらを変更したら、認証した後に新しい質問を作ることができます。

React Q&A app submit content via form secured by Auth0

それから、アプリの機能を終えるには、ユーザーが質問に答えることができるフォームを含む Question コンポーネントをリファクターします。このフォームを定義するには、次のコードで Question ディレクトリ内に SubmitAnswer.js と呼ばれる新しいファイルを作ります。

import React, {Component, Fragment} from 'react';
import {withRouter} from 'react-router-dom';
import auth0Client from '../Auth';

class SubmitAnswer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      answer: '',
    };
  }

  updateAnswer(value) {
    this.setState({
      answer: value,
    });
  }

  submit() {
    this.props.submitAnswer(this.state.answer);

    this.setState({
      answer: '',
    });
  }

  render() {
    if (!auth0Client.isAuthenticated()) return null;
    return (
      <Fragment>
        <div className="form-group text-center">
          <label htmlFor="exampleInputEmail1">Answer:</label>
          <input
            type="text"
            onChange={(e) => {this.updateAnswer(e.target.value)}}
            className="form-control"
            placeholder="Share your answer."
            value={this.state.answer}
          />
        </div>
        <button
          className="btn btn-primary"
          onClick={() => {this.submit()}}>
          Submit
        </button>
        <hr className="my-4" />
      </Fragment>
    )
  }
}

export default withRouter(SubmitAnswer);

このコンポーネントは NewQuestion コンポーネントと同じように機能します。ここでの違いは POST 要求自体を処理する代わりに、コンポーネントがそれを他の誰かにデリゲートします。また、ユーザーが認証されていなければ、このコンポーネントは何もレンダーしません。

このコンポーネントを使用するには、Question.js ファイルを開き、その内容と次とを置き換えます。

import React, {Component} from 'react';
import axios from 'axios';
import SubmitAnswer from './SubmitAnswer';
import auth0Client from '../Auth';

class Question extends Component {
  constructor(props) {
    super(props);
    this.state = {
      question: null,
    };

    this.submitAnswer = this.submitAnswer.bind(this);
  }

  async componentDidMount() {
    await this.refreshQuestion();
  }

  async refreshQuestion() {
    const { match: { params } } = this.props;
    const question = (await axios.get(`http://localhost:8081/${params.questionId}`)).data;
    this.setState({
      question,
    });
  }

  async submitAnswer(answer) {
    await axios.post(`http://localhost:8081/answer/${this.state.question.id}`, {
      answer,
    }, {
      headers: { 'Authorization': `Bearer ${auth0Client.getIdToken()}` }
    });
    await this.refreshQuestion();
  }

  render() {
    const {question} = this.state;
    if (question === null) return <p>Loading ...</p>;
    return (
      <div className="container">
        <div className="row">
          <div className="jumbotron col-12">
            <h1 className="display-3">{question.title}</h1>
            <p className="lead">{question.description}</p>
            <hr className="my-4" />
            <SubmitAnswer questionId={question.id} submitAnswer={this.submitAnswer} />
            <p>Answers:</p>
            {
              question.answers.map((answer, idx) => (
                <p className="lead" key={idx}>{answer.answer}</p>
              ))
            }
          </div>
        </div>
      </div>
    )
  }
}

export default Question;

ここではご覧のように(ユーザーの ID トークンで)バックエンド API に要求を発行する submitAnswer メソッドを定義し、refreshQuestion と呼ばれるメソッドを定義することについて見ていきます。このメソッドは初めて React がこのコンテンツ(componentDidMount)をレンダーするときと、バックエンド API が submitAnswer メソッドの POST 要求に返答する直後の2つの状況に質問のコンテンツをリフレッシュします。

Question コンポーネントをリファクタリングした後に、アプリの完成バージョンができます。それをテストするには、http://localhost:3000/ に移動し、完全な React アプリを使い始めます。サインインしたら、質問することができ、それらに回答することもできます。やりましたね。

Full React Q&A app tutorial demo

リフレッシュ後もユーザーをサインインのままにする

これは完全機能のアプリですが、アプリケーションにサイインした後、ブラウザーをリフレッシュすると、サインアウトになります。これはなぜでしょうか?トークンはメモリに保存され(指示に従って)ているので、そのメモリはリフレッシュすると、消去されるからです。最善の動作とは言えませんね。

幸運なことに、この問題を解決するのは簡単です。Auth0 が提供するサイレント認証 を利用していただきます。つまり、アプリケーションがロードされたら、現在のユーザー(実際はブラウザー)が有効なセッションを持っているかをチェックするように Auth0 にサイレント要求を送信します。持っていれば、Auth0 は丁度認証コールバックで行うように idTokenidTokenPayload をユーザーに送り返します。

サイレント認証を使用するには、AuthApp の2つのクラスをリファクターしなければなりません。ただし、これらクラスをリファクターする前に、Auth0 アカウントのいくつかの構成を変更する必要があります。

初心者の場合、Auth0 ダッシュボードのアプリケーション セクションに移動し、React アプリを表すアプリケーションを開き、次の2つのフィールドを変更します。

  1. 許可された Web オリジン:アプリが AJAX 要求を Auth0 に発行するので、http://localhost:3000 をこのフィールドに加える必要があります。ここにこの値がなければ、Auth0 はアプリから来るすべての AJAX 要求を拒否します。
  2. 許可されたログアウト URL:ユーザーが Auth0 でのセッションを終えるには、ログアウト エンドポイントを呼び出す必要があります。認証エンドポイントと同様に、ログアウト エンドポイントはプロセスの後ホワイトリストされた URL にユーザーをリダイレクトのみします。よって、このフィールドにも http://localhost:3000 を加える必要があります。

これらフィールドを更新した後、変更を保存 ボタンを押します。それから、アプリのコードを処理する前にしなければならないことは、ユーザーが Google を通して認証できるように Auth0 が使用している開発キーを置き換える必要があります。

お気づきでないかもしれませんが、Auth0 アカウントで Google に関することを構成していなくても、ソーシャルログイン ボタンがありますからそれが機能します。この機能が追加設定なしで動作する唯一の理由は、Auth0 は Google で登録された開発キーを使用するすべての新規アカウントを自動構成するからです。ただし、開発者がもっと真剣に Auth0 を使い始めたら、これらキーを独自のものと置き換えることをお勧めします。これを強制するために、アプリがサイレント認証を実行しようとし、そのアプリがその開発キーをまだ使用するとき、Auth0 は有効なセッションがないと返します(真実でなくても)。

ですから、これらキーを変更するには ダッシュボード上のソーシャルコネクションに移動し、[Google] をクリックします。 そこには、クライアント IDクライアント シークレット のフィールドがあります。ここがキーを挿入する場所です。キーを取得するには Auth0 が提供するGoogle にアプリを接続するドキュメント コメントをお読みください。

注: Google キーを使用したくない場合は、このソーシャルコネクションを非アクティブ化し、Auth0 のユーザー名およびパスワードの認証 を通してアプリをサインアップしたユーザーのみを信頼します。

これで Auth0 アカウントの構成が終わったので、コードの処理に戻ります。そこで React アプリの ./src/Auth.js ファイルを開き、次のように更新します。

import auth0 from 'auth0-js';

class Auth {
  // ... constructor, getProfile, getIdToken, isAuthenticated, signIn ...

  handleAuthentication() {
    return new Promise((resolve, reject) => {
      this.auth0.parseHash((err, authResult) => {
        if (err) return reject(err);
        if (!authResult || !authResult.idToken) {
          return reject(err);
        }
        this.setSession(authResult);
        resolve();
      });
    })
  }

  setSession(authResult) {
    this.idToken = authResult.idToken;
    this.profile = authResult.idTokenPayload;
    // set the time that the id token will expire at
    this.expiresAt = authResult.idTokenPayload.exp * 1000;
  }

  signOut() {
    this.auth0.logout({
      returnTo: 'http://localhost:3000',
      clientID: '<YOUR_AUTH0_CLIENT_ID>',
    });
  }

  silentAuth() {
    return new Promise((resolve, reject) => {
      this.auth0.checkSession({}, (err, authResult) => {
        if (err) return reject(err);
        this.setSession(authResult);
        resolve();
      });
    });
  }
}

// ... auth0Client and export ...

注: <YOUR_AUTH0_CLIENT_ID> と Auth0 アプリケーションのクライアント ID とを置き換える必要があります。このクラスのコンストラクターで auth0.WebAuth にパスしたオブジェクトの audience を構成するために使用する同じ値を使う必要があります。

このクラスの新しいバージョンで、次を実行します。

  • ユーザーの詳細 setSession を設定するメソッドを追加する
  • setSession メソッドを使う handleAuthentication メソッドをリファクタリングする
  • auth0-js が提供する checkSession 機能を呼び出す silentAuth と呼ばれるメソッドを加える(このメソッドも setSession を使用する)
  • Auth0 でのログアウトエンドポイントを呼び出す signOut 機能をリファクタリングし、その後にユーザーをリダイレクトしなければならない場所を伝える(例:returnTo: 'http://localhost:3000'

それから最後に ./src/App.js ファイルを開き、次のように更新する必要があります。

// ... other imports ...
import {Route, withRouter} from 'react-router-dom';
import auth0Client from './Auth';

class App extends Component {
  async componentDidMount() {
    if (this.props.location.pathname === '/callback') return;
    try {
      await auth0Client.silentAuth();
      this.forceUpdate();
    } catch (err) {
      if (err.error !== 'login_required') console.log(err.error);
    }
  }

  // ... render ...
}

export default withRouter(App);

ご覧のように、このファイルの新規バージョンはアプリがロードするときに機能することを定義します(componentDidMount)。

  1. 要求されたルートが /callback の場合はアプリは何もしません。これは、ユーザーが /callback ルートを要求すると、認証プロセスの後に Auth0 によってリダイレクトされるので正しい動作です。この場合、Callback コンポーネントでプロセスを処理させます。
  2. 要求されたルートが他の場合は、アプリが silentAuth を試そうとします。エラーが起きなければ、アプリはユーザーがその名前とサインインしたことを確認できるように forceUpdate を呼び出します。
  3. silentAuth にエラーがあれば、アプリはそのエラーが login_required であることをチェックします。そうであれば、アプリはユーザーがサインインしていないこと(または、使うべきでない開発キーを使っていること)なので何もしません。
  4. login_required でないエラーがあれば、そのエラーはコンソールにログされます。実際、この場合、エラーについて誰かに通知して何が起きたかを確認できるようにした方がよいです。

ところで、withRouter 関数の内にある App クラスを囲い、どのルートが呼ばれることを確認できるようにします(this.props.location.pathname)。withRouter なしでは location オブジェクトへのアクセスがありません。

認証ユーザーの Auth0 へのリダイレクトを避ける

作業を終える前に、やらなければならないことがもう一つあります。上記のソリューションは保護されたルート以外であれば、スムーズに機能します。保護されたルート(例:/new-question)で、ブラウザーを更新すると、Auth0 にリダイレクトされて再度サイインインします。ここでの問題は SecuredRoute コンポーネントは、アプリがサイレント認証のプロセスから応答を受ける前にユーザーが認証されているか否か(if (!auth0Client.isAuthenticated()))をチェックすることです。よって、アプリはユーザーが認証されていないと考え、Auth0 (auth0Client.signIn();)にリダイレクトしてサインインできるようにします。

この非行を修正するには、SecuredRoute.js ファイルを開いて、次のように更新する必要があります。

// ... import statements ...
function SecuredRoute(props) {
  const {component: Component, path, checkingSession} = props;
  return (
    <Route path={path} render={() => {
      if (checkingSession) return <h3 className="text-center">Validating session...</h3>;
      // ... leave the rest untouched ...
     }} />
  );
}

export default SecuredRoute;

ここでの違いは SecuredRoute コンポーネントがブール値が true に設定されていれば、props から来る checkingSession と呼ばれるブール値を検証し、アプリはセッションの検証中 という h3 要素を表示することです。このプロパティが false に設定されていれば、コンポーネントは以前のように動作します。

ここで、このプロパティを SecuredRoute, にパスするには、App.js ファイルを開き、次のように更新します。

// ... import statements ...
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      checkingSession: true,
    }
  }

  async componentDidMount() {
    if (this.props.location.pathname === '/callback') {
      this.setState({checkingSession:false});
      return;
    }
    // ... leave try-catch untouched
    this.setState({checkingSession:false});
  }

  render() {
    // ... leave other routes untouched ...
    // replace SecuredRoute with this:
    <SecuredRoute path='/new-question'
                  component={NewQuestion}
                  checkingSession={this.state.checkingSession} />
  }
}

export default withRouter(App);

これだけです!これらの変更が終わると、React アプリケーションの開発が終わります。ここで、サインインして、ブラウザーを更新したら(どのルートを使っていても)、セッションを失うことも、再度サインインする必要もありません。もうすぐです!

「初めての React アプリケーションを作りました。」

まとめ

本書では、たくさんの素晴らしいテクノロジーや概念で遊ぶチャンスがありました。まず、React が紹介する重要な概念(コンポーネント アーキテクチャ や JSX 構文など)について学びました。それから、Node.js と Express でバックエンド API を作る方法を手短かに学びました。その後で、React アプリケーションの作り方やその全体を Auth0 でセキュアにする方法を学びました。

本書ではたくさんのトピックを紹介してきましたので、それらを完全に理解することはできなかったと思います。例えば、コンポーネント ライフサイクル のような重要な概念のほんの一部をかろうじて取り扱いました。また、HTML 要素を操作するときに React の何がしっかりした基盤なのかを学ぶチャンスもありませんでした。残念ながら、これらトピックを深く取り扱うと膨大な量(本書よりもずっと多い)になりますから、可能ではありません。

ですから、初めての React アプリケーションを作り終えたら、チュートリアルに記載のリンクや参考を確認し、React 機能の詳細については仮想 DOM および内部 アーティクルをご覧ください。

また、ご質問等については以下のコメント欄にメッセージをご記入ください。頑張りましょう!