React 入門
JavaScript コメント掲示板アプリの開発
無限スクロールの実装
このページでは従来型のページ切り替えボタンを設置して,コメント一覧表示において表示したいページを切り替えるユーザインタフェースを作成しました.React では非同期通信を利用して,ページ内の必要箇所だけの再描画を比較的簡単に実装することができます.したがって,SNS のアプリで見られるような無限スクロールも簡単に実装できます.ここでは無限スクロールをコメント一覧ページに実装します.
無限スクロールを実装する前に,コメントのページ切り替えボタンを削除して,それに関連するコードも削除(またはコメントアウト)しておきます.
src/components/CommentListPage.js
import React from 'react'
import { useState, useEffect } from 'react';
import axios from 'axios';
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":9999,
"title":"ダミー",
"body":"ダミーの本文",
"updated_at":"2023-11-21T11:20:00"
},
{
"id":10000,
"title":"ダミーのタイトル",
"body":"ダミーです",
"updated_at":"2023-11-21T11:10:00"
}
]
});
// const [isPrevButtonDisabled, setPrevButtonDisabled] = useState(true);
// const [isNextButtonDisabled, setNextButtonDisabled] = useState(false);
const handleCommentsListChange = (url) => {
axios.get(url)
.then(res => setResults(res.data))
.catch(err => console.log(err.message));
}
useEffect(() => {
const url = "http://127.0.0.1:8000/comments/";
axios.get(url)
.then(res => setResults(res.data))
.catch(err => console.log(err.message));
}, []);
const handleDeleteButtonClick = (commentId) => {
if (!window.confirm("削除しますか?")) {
return;
}
const newComments = [...results.results].filter((comment) => {
return comment.id !== commentId;
});
const newResults = {
"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, 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); // 画面を更新
// API に POST でリクエストする
let result = false;
const url = "http://127.0.0.1:8000/comments/";
await axios
.post(url, {
title: title,
body: body,
})
.then(res => {
// console.log(res.data);
handleCommentsListChange(url);
result = true;
})
.catch(err => {
setResults(results); // 画面を元に戻す(元の配列で更新)
alert("投稿エラー!未入力または文字数超過です.");
});
return result;
}
// const handlePrevPageButton = () => {
// if (results.previous === null) {
// return;
// }
// axios.get(results.previous)
// .then(res => setResults(res.data))
// .catch(err => console.log(err.message));
// };
// const handleNextPageButton = () => {
// if (results.next === null) {
// return;
// }
// axios.get(results.next)
// .then(res => 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.js(抜粋)
return (
<div className="container">
<h1>コメント一覧</h1>
<div className="commentsHeader">
<div>コメント総数:{results.count}</div>
</div>
{commentItems}
<div className="infinityScroll">
<button>次を読み込む ({results.next})</button>
</div>
<CreateForm
onSubmit={handelCreateFormSubmit}
/>
</div>
)
ボタンが画面の左右中央に配置されるように index.css にスタイルを追加します.
src/index.css(抜粋)
.infinityScroll {
text-align: center;
margin-bottom: 20px;
}
ボタンに onClick
属性を追加します.
src/components/CommentListPage.js(抜粋)
<div className="infinityScroll">
<button
onClick={handleInfinityScrollButton}
>
次を読み込む ({results.next})
</button>
</div>
その関数を定義します.まずはクリックによってその関数が呼び出されたことを確認するためだけのコードを入力しておきます.
src/components/CommentListPage.js(抜粋)
const handleInfinityScrollButton = () => {
console.log('handleInfinityScrollButton');
}
ボタンのクリックで handleInfinityScrollButton
関数が実行されることを確認してから,無限スクロールのためのコードを追加します.
src/components/CommentListPage.js(抜粋)
const handleInfinityScrollButton = () => {
axios.get(results.next)
.then(res => {
const prevComments = [...results.results]; // 表示済のコメント配列
const moreComments = [...res.data.results]; // 新たに読み込んだコメント配列
const newResults = {
"count": res.data.count,
"previous": null, // 前のページは null のまま
"next" : res.data.next, // 次のURLを更新
"results": prevComments.concat(moreComments), // 配列を連結
};
setResults(newResults); // 画面を更新
})
.catch(err => console.log(err.message));
}
上の10行目では,すでに画面に表示済みのコメント配列 (prevComments
) と新たに読み込んだコメント配列 (moreComments
) を concat
関数で連結していることに注意してください.また次に読み込むべき URL は9行目でその都度更新されていることにも注意してください.これで無限スクロールが実現できました.実際にブラウザで動作を確認します.
一覧ページを再読み込みした状態です.「次を読み込む」というボタンをクリックすると API には 2 ページ目の内容がリクエストされます.
API から受け取った2件のコメントがすでにあるコメントの後に追加され,次の URL が 3 ページ目のものになりました.最後のページまでいわゆる無限スクロールができるようになったはずです.