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

React 入門トップページ

« 戻る 次へ »

React 入門

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

コメント投稿機能の実装

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

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

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

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

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

次に,CreateForm コンポーネントでは CommentListPage コンポーネントから渡された onSubmit 関数を props で受け取ります.このために,先頭の import文の直後(CreatForm関数定義より前)でインターフェースを定義します.現時点では onSubmit 関数から返す値はないので void にしておきます.

src/components/CreateForm.tsx(抜粋)interface CreateFormProps {
  onSubmit: () => void;
}

CreateForm 関数は CreateFormProps 型の props を引数として受け取り,関数コンポーネント React.FC を返すので,関数の定義を次の通り修正します.

src/components/CreateForm.tsx(抜粋)const CreateForm: React.FC<CreateFormProps> = (props) => {

つまり,CreateForm.tsx からは props.onSubmit()で CommentListPage の handleCreateFormSubmit を実行できることを意味します.

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

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

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

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

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

src/components/CreateForm.tsx(抜粋)const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
  event.preventDefault();
  console.log("CreateForm : handlSubmit");
  props.onSubmit();
};

上の FormEvent が利用できるように先頭でインポートします.

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

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

ts-2024-20

CreateForm コンポーネントでは,titlebody で内部状態を管理しているので,この2つを CommentListPage の関数へ渡さなければなりません.このためにインターフェースを修正します.

src/components/CreateForm.tsx(抜粋)interface CreateFormProps {
  onSubmit: (title: string, body: string) => void;
}

CreateForm コンポーネントから titlebody を引数で onSubmit 関数に渡します.

src/components/CreateForm.tsx(抜粋)const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
  event.preventDefault();
  // console.log("CreateForm : handlSubmit");
  props.onSubmit(title, body);
};

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

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

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

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

const CommentListPage: React.FC = () => {

  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: string, body: string) => {
    // console.log('CommentListPage : handelCreateFormSubmit');
    console.log('title', title);
    console.log('body', body);
  }

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

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

export default CommentListPage

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

CreateForm.tsximport React from 'react'
import { useState, FormEvent } from 'react';

interface CreateFormProps {
  onSubmit: (title: string, body: string) => void;
}

const CreateForm: React.FC<CreateFormProps> = (props) => {
  const [title, setTitle] = useState<string>('');
  const [body, setBody] = useState<string>('');

  const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setTitle(event.currentTarget.value);
  };

  const handleBodyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setBody(event.currentTarget.value);
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    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

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

ts-2024-21

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

src/components/CommentListPage.tsx(抜粋)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 をインポートします.さらに,results の型を定義するために Results というインターフェースを準備します.

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

interface CommentData {
  id: number;
  title: string;
  body: string;
  updated_at: string;
}

interface Results {
  count: number;
  next: string | null;
  previous: string | null;
  results: CommentData[];
}

const CommentListPage: React.FC = () => {

  const [results, setResults] = useState<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"
      }
    ]
  });

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

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

interface CommentData {
  id: number;
  title: string;
  body: string;
  updated_at: string;
}

interface Results {
  count: number;
  next: string | null;
  previous: string | null;
  results: CommentData[];
}

const CommentListPage: React.FC = () => {

  const [results, setResults] = useState<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: string, body: string) => {
    // console.log('CommentListPage : handelCreateFormSubmit');
    // console.log('title', title);
    // console.log('body', body);
    const newComments = [...results.results];  // スプレッド構文でコメントの配列だけを取り出す
    newComments.unshift({   // unshift で先頭に追加,push では最後に追加
      id: Date.now(),
      title: title,
      body: body,
      updated_at: "2024-03-18T12:00: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

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

ts-2024-22

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

src/components/CreateForm.tsx(抜粋)const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
  event.preventDefault();
  props.onSubmit(title, body);
  setTitle(''); // 入力フォームを空に戻す
  setBody('');  // 入力フォームを空に戻す
};

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

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

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

src/components/CreateForm.tsx(抜粋)const inputTitleRef = useRef<HTMLInputElement>(null);

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

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

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

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
  event.preventDefault();
  props.onSubmit(title, body);
  setTitle(''); // 入力フォームを空に戻す
  setBody('');  // 入力フォームを空に戻す
  if (inputTitleRef.current) {
    inputTitleRef.current.focus();  // フォーカスを当てる
  }
};

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

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

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

interface CommentData {
  id: number;
  title: string;
  body: string;
  updated_at: string;
}

interface Results {
  count: number;
  next: string | null;
  previous: string | null;
  results: CommentData[];
}

const CommentListPage: React.FC = () => {

  const [results, setResults] = useState<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: string, body: string) => {
    const newComments = [...results.results];  // スプレッド構文でコメントの配列だけを取り出す
    newComments.unshift({   // unshift で先頭に追加,push では最後に追加
      id: Date.now(),
      title: title,
      body: body,
      updated_at: "2024-03-18T12:00: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.tsx の全体も確認します.

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

interface CreateFormProps {
  onSubmit: (title: string, body: string) => void;
}

const CreateForm: React.FC<CreateFormProps> = (props) => {
  const [title, setTitle] = useState<string>('');
  const [body, setBody] = useState<string>('');
  const inputTitleRef = useRef<HTMLInputElement>(null);

  const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setTitle(event.currentTarget.value);
  };

  const handleBodyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setBody(event.currentTarget.value);
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    props.onSubmit(title, body);
    setTitle(''); // 入力フォームを空に戻す
    setBody('');  // 入力フォームを空に戻す
    if (inputTitleRef.current) {
      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 で投稿できるので,キーボード操作だけで,連続してコメントを投稿できることを試してください.

ts-2024-23

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

ts-2024-24

目次に戻る