9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactAdvent Calendar 2024

Day 2

Reactで文字・単語数を解析するアプリを作ってみたのでポイントを解説

Last updated at Posted at 2024-11-09

こんにちは、とまだです。

React アドベントカレンダー 2024、2 日目の記事をお届けします!

今回は、React で文字数や単語数を解析するアプリを作ってみました。

スクリーンショット 2024-11-09 11.12.42.png

エディタでテキストを入力すると、その文字数や単語数をリアルタイムで解析するアプリです。
また、文字数制限の警告表示クリップボードへのコピー機能も備えています。

設計やコンポーネントの分割方法など、ポイントをご紹介するので、ぜひ参考にしてください!

ユーザーのアクションに応じて、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 から読み込むイメージです。

まずは全体像を確認しましょう。

src/components/TextAnalyzer.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

こちらは、テキストの状態管理や解析を行うカスタムフックです。

src/hooks/useTextAnalysis.ts
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,
  }
}

実装のポイントは以下の通りです。

  1. useState でテキストの状態を管理。
  2. useMemo を使い、テキスト解析結果の不要な再計算を防止。
  3. handleCopy でクリップボードへのコピー機能を提供。

それぞれ、既存の React フックを活用して、状態管理や副作用を管理しています。

なお、カスタムフックを一つにまとめていますが、たとえば useTextStatsuseTextCopy など、機能ごとに分割することも可能です。

2.3 テキスト入力フィールド:TextArea

テキストを入力し、その変更を親コンポーネントに伝える役割を持つコンポーネントです。

src/components/TextArea.tsx
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%を超えた場合に警告を表示します。

スクリーンショット 2024-11-09 11.30.50.png

src/components/Warning.tsx
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 統計情報の表示:StatsGridStatItem

テキスト解析結果を見やすく表示するためのコンポーネントを紹介します。

StatsGrid

src/components/StatsGrid.tsx
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>
);

今回は、StatsGridStatItem という 2 つのコンポーネントを組み合わせて、統計情報を表示しています。

  • StatsGrid は、StatItem を 3 つ並べたグリッドレイアウト。
  • StatItem は、ラベルと値を表示する UI コンポーネント。

このように、コンポーネントを細かく分割することで、再利用性を高めているのがポイントです。

2.6 クリップボードへのコピー:CopyButton

最後に、テキストをクリップボードにコピーする機能を実装します。

src/components/CopyButton.tsx
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 年のアドベントカレンダーに参加しています。

以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです!

9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?