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

React 入門トップページ

« 戻る 次へ »

React 入門

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

無限スクロールの実装

このページでは従来型のページ切り替えボタンを設置して,コメント一覧表示において表示したいページを切り替えるユーザインタフェースを作成しました.React では非同期通信を利用して,ページ内の必要箇所だけの再描画を比較的簡単に実装することができます.したがって,SNS のアプリで見られるような無限スクロールも簡単に実装できます.ここでは無限スクロールをコメント一覧ページに実装します.

無限スクロールを実装する前に,コメントのページ切り替えボタンを削除して,それに関連するコードも削除(またはコメントアウト)しておきます.

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 handleCommentsListChange = (url: string) => {
    axios.get<Results>(url)
        .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 url = `http://127.0.0.1:8000/comments/${commentId}/`;
    axios
      .delete(url)
      .then(res => {
        if (res.status === 204) {
          handleCommentsListChange('http://127.0.0.1:8000/comments/');
        } else {
          console.log("Delete失敗");
        }
      })
      .catch(err => console.log(err.message));
  };

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

  const handelCreateFormSubmit = async (title: string, body: string):Promise<boolean> => {
    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); // 画面を更新

    let result:boolean = false;
    const url = "http://127.0.0.1:8000/comments/";
    await axios
      .post(url, {
        title: title,
        body: body,
      })
      .then(res => {
        handleCommentsListChange(url);
        result = true;
      })
      .catch(err => {
        setResults(results);
        alert("投稿エラー!未入力または文字数超過です.");
      });
    return result;
  }

  // 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

一覧表示されたコメントの直下にボタンを配置して,スタイリングのために infinityScroll というクラス名を付けておきます.また動作確認のため,次に読み込むべき URL もボタンの中に表示しておきます(動作確認後には削除してよいでしょう).さらにボタンを押したときに実行する関数も定義しておきます.

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

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

    <div className="commentsHeader">
      <div>コメント総数:{results.count}</div>
    </div>

    {commentItems}

    <div className="infinityScroll">
      <button
        onClick={handleInfinityScrollButton}
      >
        次を読み込む ({results.next})
      </button>
    </div>

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

ボタンが画面の左右中央に配置されるように index.css にスタイルを追加します.

src/index.css(抜粋).infinityScroll {
  text-align: center;
  margin-bottom: 20px;
}

ボタンのクリックで handleInfinityScrollButton 関数が実行されることを確認してから,無限スクロールのためのコードを追加します.

src/components/CommentListPage.tsx(抜粋)const handleInfinityScrollButton = () => {
  axios.get<Results>(results.next!)
    .then((res: AxiosResponse<Results>) => {
      const prevComments = [...results.results];
      const moreComments = [...res.data.results];
      const newResults: Results = {
        count: res.data.count,
        previous: null,
        next : res.data.next,
        results: prevComments.concat(moreComments),
      };
      setResults(newResults);
    })
    .catch(err => console.log(err.message));
}

上の10行目では,すでに画面に表示済みのコメント配列 (prevComments) と新たに読み込んだコメント配列 (moreComments) を concat 関数で連結していることに注意してください.また次に読み込むべき URL は9行目でその都度更新されていることにも注意してください.これで無限スクロールが実現できました.実際にブラウザで動作を確認します.

一覧ページを再読み込みした状態です.「次を読み込む」というボタンをクリックすると API には 2 ページ目の内容がリクエストされます.

ts-2024-63

API から受け取った2件のコメントがすでにあるコメントの後に追加され,次の URL が 3 ページ目のものになりました.最後のページまでいわゆる無限スクロールができるようになったはずです.

ts-2024-64

目次に戻る