はじめに
Reactの基礎学習のアウトプットとして、簡単なアプリを作成しました。
シンプルな構成ですが、Reactの基本である状態管理やコンポーネント分割を学べる内容になっていますので、React学習の参考にしていただければと思います。
アプリ概要
テキストを入力してメモを追加し、抽選ボタンを押すとメモの中からランダムで1つ選ばれるツールです。
今晩のおかずなど、何かに迷ったときに使える簡易的なおみくじアプリをイメージしています。
完成したものを以下のURLにてデプロイしているので、よければ使ってみてください。
アプリの機能
- テキストを入力してメモを追加&削除できる
- 抽選ボタンを押すと、登録したテキストメモの中からランダムで1つ表示させる
コンポーネント構成
本アプリは以下のようにコンポーネントを分割しました。
- Button:再利用可能のボタン
- MemoInput:メモの入力エリア
- MemoList:メモ一覧表示
- RandomResult:抽選結果表示
設計意図
「状態を持つか / 表示に専念するか」を基準にコンポーネントを分割しました。
- 状態を持つ → 親コンポーネント
- 表示のみ(イベントはpropsで受け取る) → 子コンポーネント
コンポーネントごとに責務を分離することで、可読性と保守性を意識しています。
また、ボタンのように複数箇所で利用するUIは共通コンポーネントとして切り出し、再利用しやすい構成を目指しました。
ファイル構成
src/
├── index.jsx
├── RandomTool.jsx
├── styles.css
└── components/
├── Button.jsx
├── MemoInput.jsx
├── MemoList.jsx
└── RandomResult.jsx
src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import RandomTool from './RandomTool';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RandomTool />
</React.StrictMode>
);
アプリ全体のエントリーポイントです。
ルートとなるコンポーネント(RandomTool)をReactにレンダリングしています。
src/RandomTool.jsx
import { useState } from "react";
import { Button } from "./components/Button";
import { MemoInput } from "./components/MemoInput";
import { MemoList } from "./components/MemoList";
import { RandomResult } from "./components/RandomResult";
import "./styles.css";
export const RandomTool = () => {
// 入力フォームの値を管理
const [text, setText] = useState("");
// メモ一覧を配列で管理
const [memos, setMemos] = useState([]);
// ランダムで表示する結果を管理
const [result, setResult] = useState("");
// 追加ボタン押下時の処理
const onClickAdd = () => {
// 空文字は追加しない(バリデーション)
if (text === "") return;
// IDを付与することで削除時に識別できるようにする
const newMemo = {
text,
id: crypto.randomUUID(),
};
// 既存配列をコピーして新しいメモを追加
setMemos([...memos, newMemo]);
// 入力欄をリセット
setText("");
};
// 削除
const onClickDelete = (id) => {
// filterを使って指定ID以外を残す
const newMemos = memos.filter((memo) => memo.id !== id);
// 新しい配列でstate更新
setMemos(newMemos);
};
// ランダム抽選処理
const onClickRandom = () => {
// メモが存在しない場合は何もしない
if (memos.length === 0) return;
// 配列の長さをもとにランダムなインデックスを生成
const randomIndex = Math.floor(Math.random() * memos.length);
// 該当するメモを結果として設定
setResult(memos[randomIndex].text);
};
return (
<>
<h2 className="title">RandomTool</h2>
<p className="title">今晩のおかずに迷ったときのための運任せツール</p>
{/* 入力コンポーネント:値とイベントをpropsで渡す */}
<MemoInput
text={text}
onChange={(e) => setText(e.target.value)}
onClick={onClickAdd}
/>
{/* メモ一覧表示 */}
<MemoList memos={memos} onDelete={onClickDelete} />
{/* 抽選ボタン */}
<Button className="result-button" onClick={onClickRandom}>
抽選する
</Button>
{/* 結果表示コンポーネント */}
<RandomResult result={result} />
</>
);
};
export default RandomTool;
アプリのメインとなる部分です。
状態を親コンポーネントで一元管理し、子コンポーネントへpropsで受け渡す構成にしています。
状態管理
useStateを使用して以下の状態を管理しています。
- 入力値(text)
const [text, setText] = useState("");
- メモ一覧(memos)
const [memos, setMemos] = useState([]);
- ランダム結果(result)
const [result, setResult] = useState("");
実装のポイント
各テキストメモにIDを付与することで、削除時に対象を一意に識別できるようにしています。
また、IDの指定に「Date.now()」ではなく「crypto.randomUUID()」を使用することで、複数のデータが同じIDとならないようにしています。
参考記事:https://zenn.dev/hina_dev/articles/crypto-randomuuid-better-solution
const newMemo = {
text,
id: crypto.randomUUID(),
};
既存の配列を直接変更せず、新しい配列を生成して更新を行っています。
setMemos([...memos, newMemo]);
配列の長さをもとにランダムなインデックスを生成しています。
const randomIndex = Math.floor(Math.random() * memos.length);
src/components/Button.jsx
export const Button = ({ className, onClick, children }) => {
return (
<button className={className} onClick={onClick}>
{children}
</button>
);
};
ボタンを共通化したコンポーネントです。 className・onClick・childrenをpropsとして受け取ることで再利用できるようにしています。
src/components/MemoInput.jsx
import { Button } from "./Button";
export const MemoInput = ({ text, onChange, onClick }) => {
return (
<div className="input-area">
{/* 入力値は親コンポーネントで管理 */}
<input
value={text}
onChange={onChange}
placeholder="テキストを入力"
/>
{/* 親から受け取った関数を実行 */}
<Button onClick={onClick}>
追加
</Button>
</div>
);
};
テキスト入力フォームのコンポーネントです。
src/components/MemoList.jsx
import { Button } from "./Button";
export const MemoList = ({ memos, onDelete }) => {
return (
<div className="list-area">
<p className="title">一覧</p>
<div className="list-text">
<ul>
{memos.map((memo) => (
<li key={memo.id}>
{/* テキストを表示 */}
{memo.text}
{/* IDを使って削除対象を特定 */}
<Button onClick={() => onDelete(memo.id)}>
削除
</Button>
</li>
))}
</ul>
</div>
</div>
);
};
メモ一覧表示エリアのコンポーネントです。
src/components/RandomResult.jsx
export const RandomResult = ({ result }) => {
return (
<div className="result-area">
<p className="title">結果</p>
{/* 初期状態ではメッセージを表示 */}
<h3 className="result-text">{result || "まだ結果がありません"}</h3>
</div>
);
};
抽選結果表示エリアのコンポーネントです。
src/styles.css
body {
font-family: sans-serif;
color: #444;
}
ul {
list-style: none;
}
.input-area {
margin: 10px;
text-align: center;
}
button {
margin-left: 5px;
margin-bottom: 5px;
padding: 5px 10px;
border-radius: 6px;
border: none;
cursor: pointer;
}
button:hover {
background-color: #6c9bcf;
color: white;
}
.list-area,
.result-area {
border: 1px solid #ccc;
padding: 10px;
margin: 10px;
border-radius: 8px;
border: 2px solid #aacfd0;
}
.result-area {
background-color: #c9dede;
}
.list-text {
text-align: center;
}
.result-text {
font-weight: bold;
margin: 10px;
text-align: center;
}
.result-button {
display: block;
margin: 30px auto;
font-size: 20px;
}
.title {
font-weight: bold;
text-align: center;
}
こちらはCSSです。
自分の好きな見栄えになるようにカスタマイズしてもいいでしょう。
Reactの強みを活かしたポイント
コンポーネント分割
UIを部品ごとに分割することでそれぞれの役割が明確になり、修正や機能追加がしやすくなりました。
状態管理と再描画
useStateで状態を管理することで、データが更新されると自動的にUIが再描画されます。
例えば、メモを追加すると一覧が即座に更新されるため、DOM操作を直接行う必要がありません。
デプロイ
デプロイはNetlifyを利用しました。
フロントエンドのアプリを簡単に公開できるため、学習のアウトプットとして非常に便利だと感じました。
参考にした記事:https://qiita.com/hisashi_matsui/items/1f4733aa7a8e410e0e38
まとめ
これまでにNuxt.js(Vue.js)を触った経験があるため、コンポーネント分割や状態管理の考え方を意識しながら実装できました。
このようなシンプルなアプリでも、
- コンポーネント分割
- useStateによる状態管理
- 配列操作によるUI構築
以上を意識するだけで、Reactの基本が一通り体験できます。
React学習初期のアウトプットとして、設計を意識した実装の練習になれば幸いです。
