React 入門
TypeScript コメント掲示板アプリの開発
コメント編集機能の実装
次に,投稿済みのコメントを編集する機能を作成します.編集は /edit/123 のようにコメント id を指定した URL で行うことにします.このためにまず,src/componenst/CommentEditPage.tsx を作成します.Visual Studio Code ではファイルの先頭に rafce と入力して次の雛形を作成します.
src/componenst/CommentEditPage.tsx
import React from 'react'
const CommentEditPage = () => {
return (
<div>CommentEditPage</div>
)
}
export default CommentEditPage
次に編集ページのためのルート情報を追加します.
src/routers/routes.tsx
import { Route, Routes } from 'react-router-dom';
import CommentListPage from '../components/CommentListPage';
import CommentShowPage from '../components/CommentShowPage';
import CommentEditPage from '../components/CommentEditPage';
const routes = (
<Routes>
<Route path="/" element={<CommentListPage />} />
<Route path="/show/:commentId" element={<CommentShowPage />} />
<Route path="/edit/:commentId" element={<CommentEditPage />} />
</Routes>
);
export default routes;
ブラウザで http://localhost:3000/edit/9/ にアクセスできることを確認します.
詳細ページから編集ページへリンクを作成するために,まず Link
をインポートします.
src/components/CommentShowPage.tsx(抜粋)
import { useParams, useNavigate, Link } from 'react-router-dom';
適当な場所に編集ページへのリンクを設置します.
src/components/CommentShowPage.tsx(抜粋)
return (
<div className="container">
<h1>コメント</h1>
<article>
<div>ID: {data.id}</div>
<div className="title">Title: {data.title}</div>
<div className="body">Body: {data.body}</div>
<div className="body">最終更新: {data.updated_at}</div>
<div>
<Link to={`/edit/${data.id}/`}>
コメント情報の編集
</Link>
</div>
</article>
<button
onClick={handleBackClick}
>コメント一覧へ戻る</button>
</div>
)
CommentEditPage コンポーネントでは URL に含まれるコメント id が取得できていることを確認します.
src/componenst/CommentEditPage.tsx
import React from 'react'
import { useParams } from 'react-router-dom';
const CommentEditPage = () => {
const {commentId} = useParams<{commentId: string}>();
return (
<div>CommentEditPage: {commentId}</div>
)
}
export default CommentEditPage
CommentShowPage コンポーネントと共通する部分が多いですが,まず API から取得した内容を画面に表示するようにします.
src/componenst/CommentEditPage.tsx
import React from 'react'
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import axios from 'axios';
interface CommentData {
id: number;
title: string;
body: string;
}
const CommentEditPage: React.FC = () => {
const {commentId} = useParams<{commentId: string}>();
const [data, setData] = useState<CommentData | null>(null);
const url = `http://127.0.0.1:8000/comments/${commentId}/`;
useEffect(() => {
axios.get<CommentData>(url)
.then(res => setData(res.data))
.catch(err => console.log(err.message));
}, [url]);
if (data === null) {
return (
<div className="container">
<h1>コメントの編集</h1>
<article>
<p></p>
</article>
</div>
);
}
return (
<div className="container">
<h1>コメントの編集</h1>
<article>
<div>ID: {data.id}</div>
<div className="title">Title: {data.title}</div>
<div className="body">Body: {data.body}</div>
</article>
</div>
)
}
export default CommentEditPage
次に,編集のためのフォームを作成します.src/components/EditForm.tsx ファイルを作成してから rafce で雛形を作成します.
src/components/EditForm.tsx
import React from 'react'
const EditForm = () => {
return (
<div>EditForm</div>
)
}
export default EditForm
CommentEditPage コンポーネントの先頭で EditForm をインポートします.
src/componenst/CommentEditPage.tsx(抜粋)
import EditForm from './EditForm';
return
の中で EditForm コンポーネントを配置します.(同時に仮に表示していたタイトルなどは削除します.)
src/componenst/CommentEditPage.tsx(抜粋)
return (
<div className="container">
<h1>コメントの編集</h1>
<EditForm />
</div>
)
EditForm コンポーネントに必要な値を渡すように修正します.
src/componenst/CommentEditPage.tsx(抜粋)
return (
<div className="container">
<h1>コメントの編集</h1>
<EditForm
commentId={data.id}
title={data.title}
body={data.body}
url={url}
/>
</div>
)
CommentEditPage コンポーネントから EditForm コンポーネントが値を受け取るようにします.このために EditFormProps
という名称でインターフェースを作成し,props
に型付けを行います.
src/components/EditForm.tsx
import React from 'react'
interface EditFormProps {
commentId: number;
title: string;
body: string;
url: string;
}
const EditForm: React.FC<EditFormProps> = (props) => {
return (
<>
<p>commentId : {props.commentId}</p>
<p>title : {props.title}</p>
<p>body : {props.body}</p>
<p>url : {props.url}</p>
</>
)
}
export default EditForm
EditForm コンポーネントにはフォームを配置し,CommentEditPage コンポーネントから渡された値をセットします.ただし,現時点ではフォームを編集できないことに注意してください.
src/components/EditForm.tsx
import React from 'react'
import { useState } from 'react';
interface EditFormProps {
commentId: number;
title: string;
body: string;
url: string;
}
const EditForm: React.FC<EditFormProps> = (props) => {
const [title, setTitle] = useState<string>(props.title);
const [body, setBody] = useState<string>(props.body);
return (
<>
<form>
<div>
<label>
タイトル:
<input
type="text"
className="textInput"
value={title}
/>
</label>
</div>
<div>
<label>
本文:
<input
type="text"
className="textInput"
value={body}
/>
</label>
</div>
<div>
<button>コメントの更新</button>
</div>
</form>
</>
)
}
export default EditForm
フォームのテキストボックスを編集できるようにします.
src/components/EditForm.tsx
import React from 'react'
import { useState, ChangeEvent } from 'react';
interface EditFormProps {
commentId: number;
title: string;
body: string;
url: string;
}
const EditForm: React.FC<EditFormProps> = (props) => {
const [title, setTitle] = useState<string>(props.title);
const [body, setBody] = useState<string>(props.body);
const handleTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
setTitle(event.currentTarget.value);
};
const handleBodyChange = (event: ChangeEvent<HTMLInputElement>) => {
setBody(event.currentTarget.value);
};
return (
<>
<form>
<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>
</>
)
}
export default EditForm
続いて,CommentEditPage にはコメントを更新するための関数を作ります.現時点では実際には更新作業を行わず,EditForm から渡されてきた情報をコンソールに表示する機能だけを実装しておきます.
src/components/CommentEditPage.tsx(抜粋)
const handleEditFormSubmit = (url: string, title: string, body: string) => {
console.log("CommentEditPage : handelEditFormSubmit");
console.log("url", url);
console.log("title", title);
console.log("body", body);
}
その関数を EditForm に渡します.
src/components/CommentEditPage.tsx(抜粋)
return (
<div className="container">
<h1>コメントの編集</h1>
<EditForm
commentId={data.id}
title={data.title}
body={data.body}
url={url}
onSubmit={handleEditFormSubmit}
/>
</div>
)
EditForm のインターフェースではその関数も受け取ることができるようにします.
src/components/EditForm.tsx(抜粋)
interface EditFormProps {
commentId: number;
title: string;
body: string;
url: string;
onSubmit: (url: string, title: string, body: string) => void;
}
EditForm コンポーネントではボタンが押されたときに formSubmit
関数が実行されるように指定します.
src/components/EditForm.tsx(抜粋)
<form onSubmit={formSubmit}>
プログラムの先頭で FormEvent
をインポートします.
src/components/EditForm.tsx(抜粋)
import { useState, ChangeEvent, FormEvent } from 'react';
EditForm でボタンが押されたときに CommentEditPage にサブミットするための関数 formSubmit
を定義します.
src/components/EditForm.tsx(抜粋)
const formSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
props.onSubmit(props.url, title, body);
};
ブラウザで現時点の動作を確認します.まず,編集画面を開くとコメントの内容がフォームに表示され,テキストボックスを書き換えることができるようになっています.
タイトルと本文を変更して「コメントの更新」ボタンを押すと,その内容がコンソールに出力されました.このとき,CommentEditPage でその内容を取得できていることにも注意してください.
ここでターミナル(またはコマンドプロンプト)から curl
コマンドでコメントの ID を指定してコメント情報を更新する方法を確認しておきます.なお,コマンド最後の /13/
にはコメントの ID を指定することに注意してください.
% curl -X PUT -d "title=API" -d "body=test" http://127.0.0.1:8000/comments/13/ ⏎
実際に API へ更新処理を送信する処理を追加しますが,更新処理後にトップページに移動したいのでまずはプログラムの先頭で useNavigate
をインポートします.
src/componenst/CommentEditPage.tsx(抜粋)
import { useParams, useNavigate } from 'react-router-dom';
更新処理をコーディングします.
src/componenst/CommentEditPage.tsx(抜粋)
const navigate = useNavigate();
const handleEditFormSubmit = (url: string, title: string, body: string) => {
axios
.put(url, {
title: title,
body: body,
})
.then(res => {
if (res.status === 200) {
console.log("PUT成功");
navigate('/');
} else {
console.log("PUT失敗");
}
})
.catch(err => {
console.log(err.message);
alert("投稿エラー!未入力または文字数超過です.");
});
}
コメントの更新ができるようになったはずなのでブラウザで確認します.コメントの編集ページでタイトルと本文を書き換えて「コメントの更新」ボタンをクリックします.
コメントが更新されて一覧ページに戻りました.
EditForm.tsx の全体を示します.
src/components/EditForm.tsx
import React from 'react'
import { useState, ChangeEvent, FormEvent } from 'react';
interface EditFormProps {
commentId: number;
title: string;
body: string;
url: string;
onSubmit: (url: string, title: string, body: string) => void;
}
const EditForm: React.FC<EditFormProps> = (props) => {
const [title, setTitle] = useState<string>(props.title);
const [body, setBody] = useState<string>(props.body);
const handleTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
setTitle(event.currentTarget.value);
};
const handleBodyChange = (event: ChangeEvent<HTMLInputElement>) => {
setBody(event.currentTarget.value);
};
const formSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
props.onSubmit(props.url, title, body);
};
return (
<>
<form onSubmit={formSubmit}>
<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>
</>
)
}
export default EditForm
CommentEditPage.tsx の全体を示します.
src/componenst/CommentEditPage.tsx
import React from 'react'
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
import EditForm from './EditForm';
interface CommentData {
id: number;
title: string;
body: string;
}
const CommentEditPage: React.FC = () => {
const {commentId} = useParams<{commentId: string}>();
const [data, setData] = useState<CommentData | null>(null);
const url = `http://127.0.0.1:8000/comments/${commentId}/`;
useEffect(() => {
axios.get<CommentData>(url)
.then(res => setData(res.data))
.catch(err => console.log(err.message));
}, [url]);
const navigate = useNavigate();
const handleEditFormSubmit = (url: string, title: string, body: string) => {
axios
.put(url, {
title: title,
body: body,
})
.then(res => {
if (res.status === 200) {
console.log("PUT成功");
navigate('/');
} else {
console.log("PUT失敗");
}
})
.catch(err => {
console.log(err.message);
alert("投稿エラー!未入力または文字数超過です.");
});
}
if (data === null) {
return (
<div className="container">
<h1>コメントの編集</h1>
<article>
<p></p>
</article>
</div>
);
}
return (
<div className="container">
<h1>コメントの編集</h1>
<EditForm
commentId={data.id}
title={data.title}
body={data.body}
url={url}
onSubmit={handleEditFormSubmit}
/>
</div>
)
}
export default CommentEditPage
なお,Python Django で開発した API 側でスリープ処理を入れている場合は,画面が切り替わるまでに時間がかかります.その間,ユーザはシステムの状態を理解することができないので,処理中であることがわかるように何らかの表示がされるようにしてもよいでしょう.