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.previous
と results.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.tsx
import 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
先頭ページでは「前のページ」へのボタンが無効化されました.
最後のページでは「次のページ」へのボタンが無効化されました.