神戸学院大学 経営学部 林坂ゼミ

React 入門トップページ

« 戻る 次へ »

React 入門

JavaScript コメント掲示板アプリの開発

コメント投稿機能の実装

いよいよ新規のコメントを追加する機能を作成します.この処理は少々難解です.

まず,コメントの一覧は CommentListPage コンポーネントで管理しているので,CommentListPage コンポーネントの中に登録処理を行う関数 (handleCreateFormSubmit) を作成します.現時点の処理内容はこの関数が実行されたことがわかるようにログに出力するだけです.

src/components/CommentListPage.js(抜粋)const handelCreateFormSubmit = () => {
  console.log('CommentListPage : handelCreateFormSubmit');
}

新たに登録するデータは CreateForm で管理されるので,CreateForm に今作成した handleCreateFormSubmit 関数を渡しますが,CreateForm では onSubmit という名称で handleCreateFormSubmit 関数が呼び出されるようにします.

src/components/CommentListPage.js(抜粋)<CreateForm
  onSubmit={handelCreateFormSubmit}
/>

次に,CreateForm コンポーネントでは CommentListPage コンポーネントから渡された onSubmit 関数を props で受け取ります.つまり,CreateForm.js からは props.onSubmit()で CommentListPage の handleCreateFormSubmit を実行できることを意味します.

src/components/CreateForm.js(抜粋)const CreateForm = (props) => {

CreateForm コンポーネントでフォームのボタンが押されたときに,CreateForm 内でこれから定義する handleSubmit 関数が実行されるようにします.

src/components/CreateForm.js(抜粋)<form onSubmit={handleSubmit}>

CreateForm で handleSubmit 関数を定義します.この中から,CommentListPage の handleCreateFormSubmit 関数を呼び出すべく props.onSubmit() を実行します.

src/components/CreateForm.js(抜粋)const handleSubmit = () => {
  console.log("CreateForm : handlSubmit");
  props.onSubmit();
}

この段階で一旦ブラウザで動作を確認します.ブラウザのコンソールには CommentListPage の handelCreateFormSubmit が実行されたことが一瞬だけ表示されます.しかし,フォームに入力した文字列が消えて,コンソールのログもすぐに消えてしまいました.これはボタンを押したことで新たなページへ遷移したためです.CommentForm に event.preventDefault() を追加することで,ボタンを押したときの標準の動作であるページの再読み込みが行われないようにできます.

src/components/CreateForm.js(抜粋)const handleSubmit = (event) => {
  event.preventDefault(); // ページ遷移しないように
  console.log("CreateForm : handlSubmit");
  props.onSubmit();
}

再びブラウザで確認すると,ボタンを押したときに実行したい関数を呼び出せていることがわかりました.

react-2024-36

CreateForm コンポーネントでは,titlebody で内部状態を管理しているので,この2つを CommentListPage の関数へ渡します.

src/components/CreateForm.js(抜粋)const handleSubmit = (event) => {
  event.preventDefault(); // ページ遷移しないように
  // console.log("CreateForm : handlSubmit");
  props.onSubmit(title, body);
}

CommentListPage の handelCreateFormSubmit 関数では,CreateForm から渡された titlebody を受け取って,ログへ出力します.

src/components/CommentListPage.js(抜粋)const handelCreateFormSubmit = (title, body) => {
  // console.log('CommentListPage : handelCreateFormSubmit');
  console.log('title', title);
  console.log('body', body);
}

現時点での CommentListPage.js の全体像を確認します.

CommentListPage.jsimport React from 'react'
import Comment from './Comment';
import CreateForm from './CreateForm';

const CommentListPage = () => {

  const results = {
    "count":10,
    "next":"http://127.0.0.1:8000/comments/?page=2",
    "previous":null,
    "results":[
      {
        "id":9,
        "title":"9個目のコメント",
        "body":"コメントの本文9",
        "updated_at":"2023-11-21T11:20:00"
      },
      {
        "id":10,
        "title":"10個目のコメント",
        "body":"コメントの本文10",
        "updated_at":"2023-11-21T11:10:00"
      }
    ]
  };

  const commentItems = results.results.map((comment) => {
    return (
      <Comment
        key={comment.id}
        comment={comment}
      />
    )
  });

  const handelCreateFormSubmit = (title, body) => {
    console.log('title', title);
    console.log('body', body);
  }

  return (
    <div className="container">
      <h1>コメント一覧</h1>
      {commentItems}

      <CreateForm
        onSubmit={handelCreateFormSubmit}
      />
    </div>
  )
}

export default CommentListPage

また,CreateForm.js の全体像も確認します.

CreateForm.jsimport React from 'react'
import { useState } from 'react';

const CreateForm = (props) => {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');

  const handleTitleChange = (event) => {
    setTitle(event.currentTarget.value);
  };

  const handleBodyChange = (event) => {
    setBody(event.currentTarget.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault(); // ページ遷移しないように
    props.onSubmit(title, body);
  }

  return (
    <>
      <hr />
      <article>
        <h2>コメントの新規投稿</h2>
        <form onSubmit={handleSubmit}>
          <div>
            <label>
              タイトル:
              <input
                type="text"
                className="textInput"
                value={title}
                onChange={handleTitleChange}
              />
            </label>
          </div>
          <div>
            <label>
              本文:
              <input
                type="text"
                className="textInput"
                value={body}
                onChange={handleBodyChange}
              />
            </label>
          </div>
          <div>
            <button>コメントの追加</button>
          </div>
        </form>
      </article>
    </>
  )
}

export default CreateForm

ブラウザで確認します.タイトルと本文を入力してボタンを押すと,その内容がコンソールに出力されました.

react-2024-37

これまでの作業で,フォームに入力されたタイトルと本文のデータを CommentListPage の handelCreateFormSubmit 関数で受け取ることができるようになりました.コメントの一覧情報は CommentListPage コンポーネントで管理しますが,現時点ではコメント一覧のオブジェクト定数 (results) は次のように定義しています.

src/components/CommentListPage.js(抜粋)const results = {
  "count":10,
  "next":"http://127.0.0.1:8000/comments/?page=2",
  "previous":null,
  "results":[
    {
      "id":9,
      "title":"9個目のコメント",
      "body":"コメントの本文9",
      "updated_at":"2023-11-21T11:20:00"
    },
    {
      "id":10,
      "title":"10個目のコメント",
      "body":"コメントの本文10",
      "updated_at":"2023-11-21T11:10:00"
    }
  ]
};

ここからは CommentListPage 内でコメントの一覧を useState() で管理しますが,初期値は上のオブジェクト定数をそのまま利用します.また,プログラムの先頭で useState をインポートします.

src/components/CommentListPage.js(抜粋)import React from 'react'
import { useState } from 'react';
import Comment from './Comment';
import CreateForm from './CreateForm';

const CommentListPage = () => {

  const [results, setResults] = useState({
    "count":10,
    "next":"http://127.0.0.1:8000/comments/?page=2",
    "previous":null,
    "results":[
      {
        "id":9,
        "title":"9個目のコメント",
        "body":"コメントの本文9",
        "updated_at":"2023-11-21T11:20:00"
      },
      {
        "id":10,
        "title":"10個目のコメント",
        "body":"コメントの本文10",
        "updated_at":"2023-11-21T11:10:00"
      }
    ]
  });

投稿機能を実装します.下の2行目では,管理されているコメントの一覧を ... というスプレッド構文で配列に展開して newComments という定数に代入しています.3行目から8行目の { ... } で囲まれる部分で,新たなコメントオブジェクトを生成しています.このとき,id には値が重複しないように投稿日時をミリ秒単位で取得したものを設定します.titlebody にはフォームから受け取った値を設定します.updated_at は現時点では利用しないので適当な日時を設定しておきます(後ほど API から受け取ることができるようになります).さらに,3行目では newComments.unshift() を使って配列の先頭に新たなコメントオブジェクトを追加しています.9行目から14行目で useState の results を更新するための新たなデータを作成します.ここでは,コメント総数が1増えるようにし,results.results に新たなコメントが追加されたオブジェクトの配列を設定します.最後に15行目でコンポーネントで管理される results の値を更新すると画面の必要部分が更新されるようになります.(3行目で unshift() の代わりに push() を使うと最後に追加されるのでこれも試してください.)

src/components/CommentListPage.js(抜粋)const handelCreateFormSubmit = (title, body) => {
  const newComments = [...results.results];  // スプレッド構文でコメントの配列だけを取り出す
  newComments.unshift({   // unshift で先頭に追加,push では最後に追加
    id: Date.now(),
    title: title,
    body: body,
    updated_at: "2024-02-25T15:30:00",
  });
  const newResults = {
    "count": results.count + 1, // コメント数は1増やす
    "previous": results.previous,
    "next" : results.next,
    "results": newComments,   // これがコメントの配列
  };
  setResults(newResults); // 画面を更新
}

ブラウザで動作を確認します.タイトルと本文を入力してボタンを押すと新たなコメントが先頭に追加されるようになりました.

react-2024-38

しかしながら,コメントを追加した後にはテキストボックスを空にしたほうが良いでしょう.CreateForm.js にテキストボックスを空にするコードを追加します.つまり,setTitle()setBody() に空の文字列を渡して内部状態を更新することで,テキストボックスをクリアできます.

src/components/CreateForm.js(抜粋)const handleSubmit = (event) => {
  event.preventDefault(); // ページ遷移しないように
  props.onSubmit(title, body);
  setTitle(''); // 入力フォームを空に戻す
  setBody('');  // 入力フォームを空に戻す
}

さらに,登録後はタイトルのテキストボックスにフォーカスが当たる(つまり入力できるように選択される)とより使いやすくなるでしょう.このためには useRef という React のフックを利用します.まず,CreateForm コンポーネントの先頭で useRef をインポートします.

src/components/CreateForm.js(抜粋)import { useState, useRef } from 'react';

次に参照オブジェクトを生成します.

src/components/CreateForm.js(抜粋)const inputTitleRef = useRef(null);

上で定義した参照オブジェクトがタイトルのテキストボックスを参照するようにします.

src/components/CreateForm.js(抜粋)<input
  type="text"
  className="textInput"
  value={title}
  onChange={handleTitleChange}
  ref={inputTitleRef}
/>

投稿完了後にテキストボックスにフォーカスを当てます.

const handleSubmit = (event) => {
  event.preventDefault(); // ページ遷移しないように
  setTitle(''); // 入力フォームを空に戻す
  setBody('');  // 入力フォームを空に戻す
  inputTitleRef.current.focus();  // フォーカスを当てる
}

これで投稿機能が完成しました.投稿したコメントが先頭に追加されます.ただし,現時点ではブラウザでページの再読み込みを行うと初期状態に戻ることを確認してください.

この時点での CommentListPage.js 全体のコードを確認します.

src/components/CommentListPage.jsimport React from 'react'
import { useState } from 'react';
import Comment from './Comment';
import CreateForm from './CreateForm';

const CommentListPage = () => {

  const [results, setResults] = useState({
    "count":10,
    "next":"http://127.0.0.1:8000/comments/?page=2",
    "previous":null,
    "results":[
      {
        "id":9,
        "title":"9個目のコメント",
        "body":"コメントの本文9",
        "updated_at":"2023-11-21T11:20:00"
      },
      {
        "id":10,
        "title":"10個目のコメント",
        "body":"コメントの本文10",
        "updated_at":"2023-11-21T11:10:00"
      }
    ]
  });

  const commentItems = results.results.map((comment) => {
    return (
      <Comment
        key={comment.id}
        comment={comment}
      />
    )
  });

  const handelCreateFormSubmit = (title, body) => {
    const newComments = [...results.results];  // スプレッド構文でコメントの配列だけを取り出す
    newComments.unshift({   // unshift で先頭に追加,push では最後に追加
      id: Date.now(),
      title: title,
      body: body,
      updated_at: "2024-02-25T15:30:00",
    });
    const newResults = {
      "count": results.count + 1, // コメント数は1増やす
      "previous": results.previous,
      "next" : results.next,
      "results": newComments,   // これがコメントの配列
    };
    setResults(newResults); // 画面を更新
  }

  return (
    <div className="container">
      <h1>コメント一覧</h1>
      {commentItems}

      <CreateForm
        onSubmit={handelCreateFormSubmit}
      />
    </div>
  )
}

export default CommentListPage

CreateForm.js の全体も確認します.

src/components/CreateForm.jsimport React from 'react'
import { useState, useRef } from 'react';

const CreateForm = (props) => {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const inputTitleRef = useRef(null);

  const handleTitleChange = (event) => {
    setTitle(event.currentTarget.value);
  };

  const handleBodyChange = (event) => {
    setBody(event.currentTarget.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault(); // ページ遷移しないように
    props.onSubmit(title, body);
    setTitle(''); // 入力フォームを空に戻す
    setBody('');  // 入力フォームを空に戻す
    inputTitleRef.current.focus();  // フォーカスを当てる
  }

  return (
    <>
      <hr />
      <article>
        <h2>コメントの新規投稿</h2>
        <form onSubmit={handleSubmit}>
          <div>
            <label>
              タイトル:
              <input
                type="text"
                className="textInput"
                value={title}
                onChange={handleTitleChange}
                ref={inputTitleRef}
              />
            </label>
          </div>
          <div>
            <label>
              本文:
              <input
                type="text"
                className="textInput"
                value={body}
                onChange={handleBodyChange}
              />
            </label>
          </div>
          <div>
            <button>コメントの追加</button>
          </div>
        </form>
      </article>
    </>
  )
}

export default CreateForm

もう一度投稿すると,テキストボックスがクリアされて,フォーカスがタイトルに当たりました.タイトルを入力後 Tab キーでフォーカスを本文に移動します.本文を入力後に Enter で投稿できるので,キーボード操作だけで,連続してコメントを投稿できることを試してください.

react-2024-39

なお,ブラウザでページを再読込すると初期状態に戻ります.

react-2024-40

目次に戻る