こんにちは、とまだです。
React アドベントカレンダー 2024、2 日目の記事をお届けします!
今回は、React で文字数や単語数を解析するアプリを作ってみました。
エディタでテキストを入力すると、その文字数や単語数をリアルタイムで解析するアプリです。
また、文字数制限の警告表示やクリップボードへのコピー機能も備えています。
設計やコンポーネントの分割方法など、ポイントをご紹介するので、ぜひ参考にしてください!
ユーザーのアクションに応じて、UI がリアルタイムに変化する機能を含んでいるので、React の基本を学べるかと思います。
ちなみに今回は、拙著で紹介している万能 React テンプレートを使用しています。
ソースコードは GitHub で公開していますので、細かい実装を確認したい方はこちらをご覧ください。
(Vitest の書き方や、GitHub ACtions による CI/CD の設定も含まれています)
0. はじめに
0.1 対象読者
- React の基本を学びたい初心者
- React Hooks の使い方を理解したい方
- コンポーネントの設計方法を学びたい方
0.2 技術スタック
- React
- TypeScript
- Tailwind CSS
- Vite
- ESLint
- Prettier
- GitHub Actions
- Vercel
1. アプリの概要と機能
このアプリは、入力されたテキストの以下の情報をリアルタイムで解析します。
- 文字数(空白含む/含まない)
- 単語数
- 文字数制限の警告表示(制限の 80%を超えた場合)
- クリップボードへのコピー
Vercel を使ってデプロイしているので、以下のリンクから実際に動作を確認できます。
2. 各コンポーネントと実装のポイント
2.1 メインコンポーネント:TextAnalyzer
アプリ全体のレイアウトとデータ管理を担当します。
ポイントは以下の通りです。
-
カスタムフック
useTextAnalysis
を使用して状態を管理。 - 子コンポーネントを組み合わせて UI を構築。
こちらを App.tsx
から読み込むイメージです。
まずは全体像を確認しましょう。
import React from 'react'
import { useTextAnalysis } from '../hooks/useTextAnalysis' // カスタムフック
import { TextAnalyzerProps } from '../types' // 型定義
import { DEFAULT_MAX_CHARS, TEXT_LABELS } from '../constants' // 定数
import { TextArea } from './TextArea'
import { Warning } from './Warning'
import { StatsGrid } from './StatsGrid'
import { CopyButton } from './CopyButton'
const TextAnalyzer: React.FC<TextAnalyzerProps> = ({
// プロパティを受け取る
maxChars = DEFAULT_MAX_CHARS,
}) => {
// カスタムフックを使用
const { text, setText, stats, isWarning, handleCopy, remainingChars } =
useTextAnalysis(maxChars)
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h1 className="text-2xl font-bold text-gray-900">
{TEXT_LABELS.TITLE}
</h1>
</div>
<div className="p-6 space-y-4">
{/* テキスト入力フィールド */}
<TextArea value={text} onChange={setText} maxLength={maxChars} />
{/* 警告メッセージ */}
{isWarning && (
<Warning maxChars={maxChars} remainingChars={remainingChars} />
)}
{/* 統計情報 */}
<StatsGrid stats={stats} />
{/* クリップボードへのコピー */}
<div className="flex justify-end">
<CopyButton onClick={handleCopy} />
</div>
</div>
</div>
</div>
</div>
)
}
export default TextAnalyzer
全体として、カスタムフックや型、定数をインポートし、それらを組み合わせて UI を構築しています。
2.2 状態管理のカスタムフック:useTextAnalysis
こちらは、テキストの状態管理や解析を行うカスタムフックです。
import { useState, useEffect, useCallback } from 'react' // React フックをインポート
import { TextStats } from '../types'
import { analyzeText, shouldShowWarning } from '../utils/textAnalyzer' // テキスト解析関数
import { DEFAULT_MAX_CHARS, WARNING_THRESHOLD } from '../constants'
// カスタムフックの定義
export const useTextAnalysis = (maxChars: number = DEFAULT_MAX_CHARS) => {
const [text, setText] = useState('') // テキストの状態
// テキスト解析結果の状態
const [stats, setStats] = useState<TextStats>({
chars: 0, // 文字数(空白含む)
charsNoSpace: 0, // 文字数(空白含まない)
words: 0, // 単語数
lines: 0, // 行数
})
const [isWarning, setIsWarning] = useState(false) // 警告表示の状態
// テキストが変更されたら解析
useEffect(() => {
const newStats = analyzeText(text) // テキスト解析
setStats(newStats) // 結果をセット
setIsWarning(shouldShowWarning(newStats.chars, maxChars, WARNING_THRESHOLD)) // 警告表示の判定
}, [text, maxChars])
// クリップボードへのコピー
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(text) // テキストをクリップボードにコピー
return true
} catch (err) {
console.error('Failed to copy text: ', err) // エラー処理
return false
}
}, [text])
return {
text,
setText,
stats,
isWarning,
handleCopy,
remainingChars: maxChars - stats.chars,
}
}
実装のポイントは以下の通りです。
-
useState
でテキストの状態を管理。 -
useMemo
を使い、テキスト解析結果の不要な再計算を防止。 -
handleCopy
でクリップボードへのコピー機能を提供。
それぞれ、既存の React フックを活用して、状態管理や副作用を管理しています。
なお、カスタムフックを一つにまとめていますが、たとえば useTextStats
と useTextCopy
など、機能ごとに分割することも可能です。
2.3 テキスト入力フィールド:TextArea
テキストを入力し、その変更を親コンポーネントに伝える役割を持つコンポーネントです。
import React from "react";
import { TEXT_LABELS } from "../constants";
interface TextAreaProps {
value: string; // テキストの値
onChange: (text: string) => void; // テキスト変更時のコールバック
maxLength: number; // 最大文字数
}
export const TextArea: React.FC<TextAreaProps> = ({ value, onChange, maxLength }) => (
// テキストエリアの実装
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full h-64 p-4 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm"
placeholder={TEXT_LABELS.PLACEHOLDER}
maxLength={maxLength}
/>
);
テキストエリアの実装には、onChange
イベントを利用して、ユーザーの入力を親コンポーネントに通知しています。
-
onChange
イベントを利用し、ユーザーの入力を親に通知。 - 最大文字数を
maxLength
で制限し、ユーザーの入力ミスを防ぎます。
React ではコンポーネントをよく分割するため、データの受け渡し方は抑えておきたいポイントです。
2.4 警告メッセージ:Warning
今回のアプリでは、入力可能な文字数に制限を設けています。
入力内容に応じて警告を表示する、という UI はよくあるので実装例を紹介します。
文字数制限の 80%を超えた場合に警告を表示します。
import React from 'react'
import { TEXT_LABELS } from '../constants'
// 警告メッセージのプロパティ
interface WarningProps {
maxChars: number // 最大文字数
remainingChars: number // 残り文字数
}
// 警告メッセージのコンポーネント
export const Warning: React.FC<WarningProps> = ({
maxChars,
remainingChars,
}) => (
<div className="bg-yellow-50 border border-yellow-400 rounded-lg p-4 text-yellow-800">
{TEXT_LABELS.WARNING_MESSAGE.replace('{max}', maxChars.toString()).replace(
'{remaining}',
remainingChars.toString()
)}
</div>
)
受け取ったプロパティに応じて UI を変化させるコンポーネントです。
条件付きレンダリングにより、特定条件でのみ表示される UI を実装する際に活用できます。
2.5 統計情報の表示:StatsGrid
とStatItem
テキスト解析結果を見やすく表示するためのコンポーネントを紹介します。
StatsGrid
:
import React from 'react'
import { StatItem } from './StatItem'
import { TextStats } from '../types'
import { TEXT_LABELS } from '../constants'
interface StatsGridProps {
stats: TextStats // テキスト解析結果
}
// 統計情報のコンポーネント
export const StatsGrid: React.FC<StatsGridProps> = ({ stats }) => (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<StatItem label={TEXT_LABELS.CHARS_WITH_SPACES} value={stats.chars} />
<StatItem
label={TEXT_LABELS.CHARS_WITHOUT_SPACES}
value={stats.charsNoSpace}
/>
<StatItem label={TEXT_LABELS.WORD_COUNT} value={stats.words} />
</div>
)
StatItem
:
import React from "react";
// 統計情報のアイテム
interface StatItemProps {
label: string; // ラベル
value: number; // 値
}
export const StatItem: React.FC<StatItemProps> = ({ label, value }) => (
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="text-sm text-gray-500">{label}</div>
<div className="text-2xl font-bold text-gray-900">{value}</div>
</div>
);
今回は、StatsGrid
と StatItem
という 2 つのコンポーネントを組み合わせて、統計情報を表示しています。
-
StatsGrid
は、StatItem
を 3 つ並べたグリッドレイアウト。 -
StatItem
は、ラベルと値を表示する UI コンポーネント。
このように、コンポーネントを細かく分割することで、再利用性を高めているのがポイントです。
2.6 クリップボードへのコピー:CopyButton
最後に、テキストをクリップボードにコピーする機能を実装します。
import React from 'react'
import { TEXT_LABELS } from '../constants'
interface CopyButtonProps {
onClick: () => void // クリック時のコールバック
}
export const CopyButton: React.FC<CopyButtonProps> = ({ onClick }) => (
<button
onClick={onClick}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
{TEXT_LABELS.COPY_BUTTON}
</button>
)
CopyButton
は、クリック時に親コンポーネントに通知するだけのシンプルなコンポーネントです。
単純にコピーするだけなら親コンポーネントに通知する必要はありませんが、コンポーネントの再利用性を考えて、クリック時の挙動を親コンポーネントに委ねるようにしています。
3. まとめ
今回の記事では、React 初心者でも理解しやすいように、アプリの主要な部分を具体的なコードとともに解説しました。
特に、React Hooks を活用した状態管理やコンポーネントの役割分担が学べたと思います。
必要最低限の機能にしてありますが、以下のような機能を追加しても面白いかもしれません。
- ローカルストレージでのデータ保存: リロードしても入力内容が消えないように
- コピー時に通知メッセージ: コピーが完了したことをユーザーに通知
- ダークモード: Tailwind CSS のクラスを切り替えて実装
4. ちょっと宣伝
Qiita だけじゃなく、個人ブログでも React に関する記事を書いています。
お時間があれば読んでいただけると、大変励みになります 🙇
5. 他にもアドベントカレンダー記事を書いています!
他にも、2024 年のアドベントカレンダーに参加しています。
以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです!