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

React 入門トップページ

« 戻る 次へ »

React 入門

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

ボタンの有効化/無効化の切り替え

ここまでの作業で,API から取得したコメント一覧を React 上でページを遷移することなく非同期的に更新することができるようになりました.ただ,最初のコメントを表示しているときには「前のページ」ボタンを無効化し,最後のコメントを表示しているときには「次のページ」ボタンを無効化しておくとより使いやすくなるでしょう.

ボタンの有効化/無効化の状態を管理するにもやはり State を利用するとよいでしょう.まず,それぞれのボタンを無効化するかどうかの変数を useState() で定義します.ここで,最初に読み込まれたときには「前のページ」ボタンは無効化しておくべきであるので,その初期値を true にします.一方で「次のページ」ボタンの初期状態は無効化しないため初期値を false にします.

src/components/CommentListPage.tsx(抜粋)const [isPrevButtonDisabled, setPrevButtonDisabled] = useState<boolean>(true);
const [isNextButtonDisabled, setNextButtonDisabled] = useState<boolean>(false);

次に,それぞれのボタン要素に disabled 属性を追加します.ここで,disabled=true となればボタンが無効化され,disabled=false となればボタンが有効化されることになります.

src/components/CommentListPage.tsx(抜粋)<div className="container">
  <h1>コメント一覧</h1>

  <div className="commentsHeader">
    <div>コメント総数:{results.count}</div>
    <div>
      <button
        onClick={handlePrevPageButton}
        disabled={isPrevButtonDisabled}
      >前のページ ({results.previous})</button>
      <button
        onClick={handleNextPageButton}
        disabled={isNextButtonDisabled}
      >次のページ ({results.next})</button>
    </div>
  </div>
</div>

では,ボタンの状態を切り替える処理を考えます.これはコメントの一覧が更新されたタイミングで判断できるので,results が再描画されたタイミングで処理すればよいことになります.よって,次のuseEffect() 関数を使い,依存する変数には results を指定します.

useEffect(()=>{描画時に実行する処理}, 依存する変数);

具体的には次のコードでボタンの有効化/無効化を切り替えます.results に関連するコンポーネントが再描画されたときに,results.previousresults.next に URL の情報が存在すれば(つまり null でなければ)ボタンの無効化変数を false にし,そうでなければ true にします.これによって,ボタンの状態が切り替えられます.

src/components/CommentListPage.tsx(抜粋)useEffect(() => {
  if (results.previous !== null) {
    setPrevButtonDisabled(false);
  } else {
    setPrevButtonDisabled(true);
  }
  if (results.next !== null) {
    setNextButtonDisabled(false);
  } else {
    setNextButtonDisabled(true);
  }
}, [results]);

ブラウザで実行して,ページを切り替えたときにボタンの有効化/無効化が適切に切り替わることを確認します.なお,上のコードは次のようにスッキリと書くことも可能です.

src/components/CommentListPage.tsx(抜粋)useEffect(() => {
  setPrevButtonDisabled(results.previous === null);
  setNextButtonDisabled(results.next === null);
}, [results]);

うまく動作すればボタンに表示していた URL の情報は削除してよいでしょう.この時点でのコード全体を示します.

src/components/CommentListPage.tsximport React from 'react'
import { useState, useEffect } from 'react';
import axios, { AxiosResponse } from 'axios';
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 [isPrevButtonDisabled, setPrevButtonDisabled] = useState<boolean>(true);
  const [isNextButtonDisabled, setNextButtonDisabled] = useState<boolean>(false);

  const [results, setResults] = useState<Results>({
    "count":10,
    "next":"http://127.0.0.1:8000/comments/?page=2",
    "previous":null,
    "results":[
      {
        "id":9999,
        "title":"ダミー",
        "body":"ダミーの本文9",
        "updated_at":"2023-11-21T11:20:00"
      },
      {
        "id":10000,
        "title":"ダミーのタイトル",
        "body":"ダミーの本文10",
        "updated_at":"2023-11-21T11:10:00"
      }
    ]
  });

  useEffect(() => {
    const url = "http://127.0.0.1:8000/comments/";
    axios.get<Results>(url)
        // .then((res: AxiosResponse<Results>) => console.log(res.data))
        .then((res: AxiosResponse<Results>) => setResults(res.data))
        .catch(err => console.log(err.message));
  }, []);

  const handleDeleteButtonClick = (commentId: number) => {
    if (!window.confirm("削除しますか?")) {
      return;
    }
    const newComments = [...results.results].filter((comment) => {
      return comment.id !== commentId;
    });
    const newResults: Results = {
      "count": results.count - 1, // コメント数は1減らす
      "previous": results.previous,
      "next" : results.next,
      "results": newComments
    };
    setResults(newResults);
  };

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

  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); // 画面を更新
  }

  const handlePrevPageButton = () => {
    if (results.previous === null) {
      return;
    }
    axios.get<Results>(results.previous!)
        .then((res: AxiosResponse<Results>) => setResults(res.data))
        .catch(err => console.log(err.message));
  };

  const handleNextPageButton = () => {
    if (results.next === null) {
      return;
    }
    axios.get<Results>(results.next!)
        .then((res: AxiosResponse<Results>) => setResults(res.data))
        .catch(err => console.log(err.message));
  };

  useEffect(() => {
    setPrevButtonDisabled(results.previous === null);
    setNextButtonDisabled(results.next === null);
  }, [results]);

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

      <div className="commentsHeader">
        <div>コメント総数:{results.count}</div>
        <div>
          <button
            onClick={handlePrevPageButton}
            disabled={isPrevButtonDisabled}
          >
            前のページ
          </button>
          <button
            onClick={handleNextPageButton}
            disabled={isNextButtonDisabled}
          >
            次のページ
          </button>
        </div>
      </div>

      {commentItems}

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

export default CommentListPage

先頭ページでは「前のページ」へのボタンが無効化されました.

ts-2024-38

最後のページでは「次のページ」へのボタンが無効化されました.

ts-2024-39

目次に戻る