Mini Project: Real-Time Character Counter and Text Utility0%
Understanding Common React Errors and Warnings

Mini Project: Real-Time Character Counter and Text Utility

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Mini Project: Real-Time Character Counter and Text Utility

1. Project Overview & Requirements

In this mini-project, we will engineer a high-utility Real-Time Character Counter and Text Utility application.

This project demonstrates the power of controlled inputs, derived state calculations, and clipboard interactions:

  1. 1
    Real-Time Metrics: Compute total characters, characters excluding whitespace, total words, sentences, and estimated reading time on every keystroke.
  2. 2
    Character Limit & Warning: Set an optional maximum character limit (e.g. 280 characters for Twitter/X) with dynamic progress bar and color threshold warnings.
  3. 3
    Text Case Transformations: Provide instant transformations (UPPERCASE, lowercase, Title Case, and Whitespace Trimming).
  4. 4
    Copy to Clipboard: Allow users to copy the transformed text with instant visual feedback.
Output
┌────────────────────────────────────────────────────────┐
│ Real-Time Text Intelligence │
│ │
│ [ Textarea: Type or paste content here... ] │
│ │
│ Progress: [████████████░░░░░░░░] 142 / 280 chars │
│ │
│ [ 24 Words ] [ 142 Chars ] [ 1m Read ] [ 2 Sent ] │
│ │
│ [ UPPERCASE ] [ lowercase ] [ Trim ] [ Copy ] │
└────────────────────────────────────────────────────────┘

2. Component Implementation

JSX
import React, { useState } from 'react';
 
const MAX_LIMIT = 280;
 
export default function TextUtility() {
const [text, setText] = useState('');
const [copied, setCopied] = useState(false);
 
// Derived metrics (computed on the fly during render!)
const charCount = text.length;
const charsNoSpaces = text.replace(/\s+/g, '').length;
const wordCount = text.trim() === '' ? 0 : text.trim().split(/\s+/).length;
const sentenceCount = text.trim() === '' ? 0 : text.split(/[.!?]+/).filter(Boolean).length;
const readingTimeMinutes = Math.ceil(wordCount / 200); // Avg reading speed 200 wpm
 
const remainingChars = MAX_LIMIT - charCount;
const percentUsed = Math.min((charCount / MAX_LIMIT) * 100, 100);
 
// Transformations
const handleUppercase = () => setText(text.toUpperCase());
const handleLowercase = () => setText(text.toLowerCase());
const handleTrimSpaces = () => setText(text.replace(/\s+/g, ' ').trim());
const handleClear = () => setText('');
 
const handleCopy = async () => {
if (!text) return;
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
 
return (
<div className="utility-card">
<h2>Text Analytics & Transformation Studio</h2>
 
{/* Controlled Textarea */}
<textarea
rows={5}
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type or paste your text here..."
className={`text-input ${charCount > MAX_LIMIT ? 'input-over-limit' : ''}`}
/>
 
{/* Progress Bar */}
<div className="progress-bar-container">
<div
className={`progress-fill ${percentUsed >= 100 ? 'bg-danger' : percentUsed >= 80 ? 'bg-warning' : 'bg-primary'}`}
style={{ width: `${percentUsed}%` }}
/>
</div>
 
<div className="limit-info">
<span>{charCount} / {MAX_LIMIT} characters</span>
<span className={remainingChars < 0 ? 'text-danger font-bold' : ''}>
{remainingChars >= 0 ? `${remainingChars} remaining` : `${Math.abs(remainingChars)} characters over limit`}
</span>
</div>
 
{/* Metrics Row */}
<div className="metrics-grid">
<div className="metric-box">
<span className="metric-val">{wordCount}</span>
<span className="metric-lbl">Words</span>
</div>
<div className="metric-box">
<span className="metric-val">{charsNoSpaces}</span>
<span className="metric-lbl">No Spaces</span>
</div>
<div className="metric-box">
<span className="metric-val">{sentenceCount}</span>
<span className="metric-lbl">Sentences</span>
</div>
<div className="metric-box">
<span className="metric-val">~{readingTimeMinutes} min</span>
<span className="metric-lbl">Read Time</span>
</div>
</div>
 
{/* Actions Toolbar */}
<div className="toolbar">
<button onClick={handleUppercase} disabled={!text}>UPPERCASE</button>
<button onClick={handleLowercase} disabled={!text}>lowercase</button>
<button onClick={handleTrimSpaces} disabled={!text}>Trim Spaces</button>
<button onClick={handleClear} disabled={!text} className="btn-danger">Clear</button>
<button onClick={handleCopy} disabled={!text} className="btn-copy">
{copied ? '✓ Copied!' : 'Copy to Clipboard'}
</button>
</div>
</div>
);
}

3. Key React Lessons Applied

  1. 1
    Zero Redundant State: Notice we did not create separate state variables for wordCount, charCount, or readingTime. All of these are calculated derived values computed cleanly during render.
  2. 2
    Asynchronous Clipboard API: Using navigator.clipboard.writeText(text) allows native browser copying, followed by a temporary 2-second visual confirmation via setTimeout.

Interactive Quiz

Practice Quiz

Question 1
Why were wordCount, charCount, and sentenceCount NOT placed inside separate useState variables?
Question 2
How is the progress bar fill width dynamically applied to the element?
Question 3
How is the temporary "Copied!" feedback message cleared after 2 seconds?
Question 4
What does text.trim().split(/\s+/).length achieve when calculating word count?
Question 5
Why is disabled={!text} applied to the transformation buttons?
Next Lesson

Understanding Common React Errors and Warnings

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext