はじめに

こんにちは。西日本テクノロジー&イノベーション室の齊藤です。

私の所属する西日本テクノロジー&イノベーション室では、2020年9月末に「SPA + REST API構成のサービス開発リファレンス(以下サービス開発リファレンス)」を公開しました。こちらは、シングルページアプリケーション(以下SPA)とREST APIから構成されるWebアプリケーションを開発する際に活用して頂けるコンテンツになっています。

前回のブログでは、SPAとREST APIをつなぎ合わせて「Hello World」を表示するところまでご紹介しました。
今回はSPAに絞って、タスク管理をするための簡易的な画面(Todo画面)を作成します。

  1. サービス開発リファレンスを使ってWebアプリケーションを作成してみよう<導入編>
  2. サービス開発リファレンスを使って1画面作成してみよう(←今回の記事)

    事前準備

    前回のブログの「サービス開発リファレンスを使ってWebアプリケーションを作成してみよう<導入編>」の完成状態をベースに作成しますので、こちらの作業がまだの方は実施してみてください。

    ページ外観の作成

    まずはTodoの内容を静的に定義して、次のような画面を作成します。

    Reactではスタイルの記述方法がいくつか提供されていますが、CSSファイルをそのまま使えるようにclassName属性とCSSファイルを使ってスタイルを定義します。

    次のようにCSSファイルを作成します。

    frontend/src/example/components/pages/Todo.css

    .content {
      margin-top: 10px;
      width: 40%;
      padding: 0 30%;
    }
    .form_field {
      margin-top: 20px;
      margin-bottom: 20px;
    }
    .form {
      width: 100%;
      display: flex;
      justify-content: space-between;
    }
    .input_field {
      width: 86%;
    }
    .input_field input{
      float: left;
      width: 95%;
      border-radius: 5px;
      padding: 8px;
      border: solid 1px lightgray;
      background-color: #fafbfc;
      font-size: 16px;
      outline: none;
    }
    .input_field input:focus {
      background-color: white;
    }
    .button_field {
      text-align: center;
      width: 14%;
    }
    .button_field button {
      height: 35px;
      cursor: pointer;
      line-height: 1;
      font-size: 1rem;
      color: white;
      background-color: black;
      border-radius: 5px;
      padding: 0 15px;
      border: none;
      vertical-align: middle;
    }
    .list {
      list-style: none;
      padding: 0;
      margin: 20px 0;
    }
    .item {
      padding: 15px 10px;
      background: whitesmoke;
      border: solid 1px lightgray;
      margin-bottom: 10px;
    }
    .todo {
      margin-left: 10px;
      text-align: left;
    }
    

    次に、Todoを表示するためのコンポーネントを作成します。
    Todoの内容は現時点では静的に定義し、後ほど登録できるようにします。

    frontend/src/example/components/pages/Todo.tsx

    import React from 'react';
    import './Todo.css';
    
    const Todo: React.FC = () => {
      
      return (
          <div className="content">
            <div className="form_field">
              <form className="form">
                <div className="input_field">
                  <input type="text" placeholder="やることを入力してください" />
                </div>
                <div className="button_field">
                  <button type="button">追加</button>
                </div>
              </form>
            </div>
            <ul className="list">
              <li className="item">
                <div className="todo">
                  <span>洗濯物を干す</span>
                </div>
              </li>
              <li className="item">
                <div className="todo">
                  <span>部屋を掃除する</span>
                </div>
              </li>
            </ul>
          </div>
      );
    };
    
    export default Todo;

    次に、frontend/src/App.tsx を以下のように変更して、先ほど作成したTodoのコンポーネントを表示するようにします。

    import React from 'react';
    import { Logger } from './framework/logging';
    import './App.css';
    import Todo from "./example/components/pages/Todo";
    
    const App = () => {
      Logger.debug('rendering App...');
      return (
          <Todo />
      );
    };
    
    export default App;

    ここで一度、frontend ディレクトリで次のコマンドを打って動作確認をします。

    $ npm start

    自動でブラウザが立ち上がり、Todoアプリの画面が表示されていれば成功です。
    ※ 自動で立ち上がらない場合は以下URLをブラウザで表示してみてください。
    http://localhost:3000/

    コンポーネントの実装

    現時点ではTodoの内容を静的に定義しているため、今度は動的にTodoを表示できるようにします。

    stateの利用

    Reactでは、stateを使用することで状態の変化を表現できます(参考:React – Reactの流儀 Step 3

    Reactの関数コンポーネントでは、様々な機能を実装するためにフック(Hooks)と呼ばれる機能が提供されており、stateの管理にはステートフックを使います(参考:ステートフックの利用法

    ステートフックはuseStateを呼び出すことで使用できます。
    引数に初期値を指定し、返り値としてstateとそれを更新するための関数をペアで返します。

    const [todos, setTodos] = useState<string[]>([]);

    また、関数コンポーネントでは、データの取得や更新によりコンポーネントに影響を与えることを副作用と呼び、副作用を起こす処理を実装するためのフックとして、副作用フックが提供されています(参考:React – 副作用フックの利用方法

    副作用フックはuseEffectを呼び出すことで使用します。
    第1引数に副作用を起こす関数と、第2引数にこの副作用が依存する値を配列で渡します。第2引数に渡した値が更新されると、第1引数の関数が実行されます。
    最初のレンダー後に1度だけ呼び出したい場合には、空の配列([])を渡します(参考:React – 副作用を使う場合のヒント(最後の補足)

    REST APIの呼び出し(※)を想定し、副作用フックを使用して静的データでstateを更新するように実装します。
    ※ REST APIとの繋ぎこみは、次回以降に実装します。

    useEffect(() => {
        setTodos([ '洗濯物を干す', '部屋を掃除する']);
      }, []);

    次に、Todoの内容を静的に定義している箇所を、stateから取得して表示するように変更します。

    <ul className="list">
      {todos.map((todo, index) =>
          <li className="item" key={index}>
            <div className="todo">
              <span>{todo}</span>
            </div>
          </li>
      )}
    </ul>

    ここまで実装すると、Todo.tsxは次のようになっています。

    import React, {useEffect, useState} from 'react';
    import './Todo.css';
    
    const Todo: React.FC = () => {
      const [todos, setTodos] = useState<string[]>([]);
    
      useEffect(() => {
        setTodos([ '洗濯物を干す', '部屋を掃除する']);
      }, []);
    
      return (
          <div className="content">
            <div className="form_field">
              <form className="form">
                <div className="input_field">
                  <input type="text" placeholder="やることを入力してください" />
                </div>
                <div className="button_field">
                  <button type="button">追加</button>
                </div>
              </form>
            </div>
            <ul className="list">
              {todos.map((todo, index) =>
                  <li className="item" key={index}>
                    <div className="todo">
                      <span>{todo}</span>
                    </div>
                  </li>
              )}
            </ul>
          </div>
      );
    };
    
    export default Todo;

    ここで、frontend ディレクトリで次のコマンドを打って動作確認をします。

    $ npm start

    自動でブラウザが立ち上がり、さきほどと同じTodoアプリの画面が表示されていれば成功です。

    useInputの利用

    次に、テキストボックスにTodoの内容を入力して追加ボタンを押すことで、Todoが追加されるようにします。

    テキストボックスを実装するために、example-chatではuseInputという独自のフック(frontend/src/framework/hooks/index.ts)を実装していますので、このフックを使用します。

    useInputでは、useStateと同様に呼び出し時に初期値を渡します。戻り値としては、state自体や、inputに渡すためのプロパティ(value, onChange属性など)が設定されたオブジェクト等が返されます。
    そしてスプレッド構文を使用して、返却されたプロパティのオブジェクトをinput要素に設定します。

    const [text, textAttributes] = useInput('');
    
    ~~~~~
    
    

    次に、サブミット時にTodoを追加する処理を実行するためにhandleSubmit関数を実装し、formのonSubmitに設定します。これで、サブミット時にこの関数がコールバックされます。
    また、サブミット時に関数がコールバックされた後、そのままだとサブミットイベントによりフォームをサーバに送信しようとしてしまうので、関数内でevent.preventDefault()を呼び、サブミットイベントをキャンセルしておきます。

    const handleSubmit = (event: React.FormEvent) => {
      event.preventDefault();
      setTodos([...todos, text]);
    };
    
    ~~~~~
    
    <<form className="form" onSubmit={handleSubmit}>

    次に、フォームのサブミットで処理を行うように、「追加」ボタンのtypesubmitに設定します。
    これで、「追加」ボタンをクリックするとサブミットされるようになります。

    <button type="submit">追加</button>

    ここまで実装すると、Todo.tsxは次のようになっています。

    import React, {useEffect, useState} from 'react';
    import {useInput} from "../../../framework";
    import './Todo.css';
    
    const Todo: React.FC = () => {
      const [todos, setTodos] = useState<string[]>([]);
      const [text, textAttributes] = useInput('');
    
      useEffect(() => {
        setTodos([ '洗濯物を干す', '部屋を掃除する']);
      }, []);
    
      const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        setTodos([...todos, text]);
      };
    
      return (
          <div className="content">
            <div className="form_field">
              <form className="form" onSubmit={handleSubmit}>
                <div className="input_field">
                  <input type="text" {...textAttributes} placeholder="やることを入力してください" />
                </div>
                <div className="button_field">
                  <button type="submit">追加</button>
                </div>
              </form>
            </div>
            <ul className="list">
              {todos.map((todo, index) =>
                  <li className="item" key={index}>
                    <div className="todo">
                      <span>{todo}</span>
                    </div>
                  </li>
              )}
            </ul>
          </div>
      );
    };
    
    export default Todo;

    ここまで実装できたら、テキストボックスにTodoの内容を入力して追加ボタンを押してみましょう。
    入力した内容が、一番下に追加されているはずです。

    ※ 今回ご紹介した useInput 以外にもラジオボタンやチェックボックス、テキストエリアなどに対応したフックもご用意しています。詳しい使い方は frontend/src/framework/hooks/index.ts のJSDocをご覧ください。

    ルーティングの設定

    SPAでは1つのページを動的に書き換えるため、ページ内容が書き換わってもそのままではURLは変わりません。しかし、ブックマークやページ履歴を利用したい場合等、ページ内容に応じてURLを変更したい場面があります。
    トップページと前回作成したHello Worldを表示する画面、そして今回作成したTodoページのURLを分けてみます。

    • / :トップページ
    • /todo :Todoページ
    • /hello :Hello Worldページ

    ルーティングを実現するために、React用のルーティングライブラリであるReact Routerを使用します。

    React Routerを使用することで、URLごとに使用するコンポーネントを制御したり異なるURLへ移動したりといったことを、簡単に実装できます。

    frontend/src/App.tsx を次のように変更します。
    (Topページは簡易にするためApp.tsxに直接書きましたが、コンポーネントに分けて実装してもよいです)

    import React from 'react';
    import { Logger } from './framework/logging';
    import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
    import HelloWorld from "./example/components/pages/HelloWorld";
    import Todo from "./example/components/pages/Todo";
    import './App.css';
    
    const App = () => {
      Logger.debug('rendering App...');
      return (
          <Router>
            <Switch>
              <Route exact path="/">
                <div>Topページ</div>
              </Route>
              <Route exact path="/todo">
                <Todo />
              </Route>
              <Route exact path="/hello">
                <HelloWorld />
              </Route>
            </Switch>
          </Router>
      );
    };
    
    export default App;

    これで準備はできましたので、frontend ディレクトリで次のコマンドを打って動作確認をします。
    ※ すでに起動している場合は、ブラウザをリロードするだけで大丈夫です。

    $ npm start

    自動でブラウザが立ち上がり、Topページが表示されていると思います。

    では次に、以下のURLを開いてみましょう。
    http://localhost:3000/todo

    Todoページが表示されていれば成功です。
    このように、簡単にURLで画面を分けることができました。

    ※ URLで切り替えることはできましたが、画面のtitle要素(上記画像ではHello World)が切り替わらないため、変更する場合はexample-chatで用意しているusePageTitleという独自のフック(frontend/src/framework/hooks/index.ts)を各コンポーネントで使用することで変更できます。

    usePageTitle('Todoページ');

    まとめ

    今回は、サービス開発リファレンスを使ってTodoページを実装しました。
    次回はAPI作成の手順をご紹介します。


    本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。