@galiprandi/react-tools is a lightweight, dependency-free utility library for React that provides reusable components and hooks to simplify development and improve accessibility.
When to Use
Use this library when you need:
- Async data handling with loading/error states →
AsyncBlock - Form management with automatic value collection →
Form,Input - Date/time inputs with ISO string handling →
DateTime - Accessible dialogs/modals →
Dialog - Viewport visibility tracking →
Observer,LazyRender - Browser-native AI features (Chrome AI API) →
useAI,useAISummarize,useLanguageDetection,useTranslator - Common React patterns →
useDebounce,useTimer,useList - Utility functions → dates, strings, userLanguage detection
Installation
npm install @galiprandi/react-tools
# or
yarn add @galiprandi/react-tools
# or
pnpm add @galiprandi/react-tools
Quick Start
import { AsyncBlock, Form, Input, useDebounce } from '@galiprandi/react-tools';
// Async data with automatic cancellation
<AsyncBlock
promiseFn={() => fetch('/api/data').then(res => res.json())}
pending={<p>Loading...</p>}
success={(data) => <p>{data.name}</p>}
error={(err) => <p>Error: {(err as Error).message}</p>}
/>
// Form with automatic value collection
<Form<{ email: string }> onSubmitValues={console.log}>
<Input name="email" label="Email" transform="onlyEmail" />
<button type="submit">Submit</button>
</Form>
// Debounced value
const debouncedSearch = useDebounce(searchTerm, 500);
Components
AsyncBlock
Declarative component for rendering async data with loading, success, and error states. Automatically cancels in-flight requests when dependencies change.
<AsyncBlock
promiseFn={() => fetch(`/api/user/${userId}`).then(res => res.json())}
deps={[userId]}
timeOut={5000}
pending={<p>Loading user...</p>}
success={(user) => <p>Welcome, {user.name}!</p>}
error={(err) => <p>Error: {(err as Error).message}</p>}
onSuccess={(user) => console.log('User loaded:', user)}
onError={(err) => console.error('Failed to load:', err)}
/>
Key Props:
promiseFn: Async function returning a Promisedeps: Dependency array for re-executiontimeOut: Optional timeout in millisecondspending/success/error: UI for each stateonSuccess/onError: Callbacks
Form
Enhanced <form> element that automatically gathers and returns values on submit.
<Form<{ username: string; email: string }>
onSubmitValues={(values) => console.log(values)}
filterEmptyValues
>
<Input name="username" label="Username" transform="titleCase" />
<Input name="email" label="Email" transform="onlyEmail" />
<button type="submit">Submit</button>
</Form>
Key Props:
onSubmitValues: Handles form submission with collected valuesfilterEmptyValues: Remove empty fields before submission
Input
Custom input component supporting transformations, debounce, datalist, and more.
<Input
label="Email"
name="email"
placeholder="Enter your email"
transform="onlyEmail"
onChangeValue={(val) => console.log(val)}
debounceDelay={500}
datalist={['user@example.com', 'admin@example.com']}
/>
Transform Options:
titleCase,toUpperCase,toLowerCasecapitalize,snakeCaseonlyNumbers,onlyLetters,onlyEmail,onlyAlphanumeric
DateTime
Wrapper around <input type="datetime-local" /> that handles ISO string conversion.
<DateTime
label="Appointment"
isoValue={appointmentDate}
onChangeISOValue={setAppointmentDate}
/>
Dialog
Accessible dialog/modal component built on the native <dialog> element.
<Dialog
behavior="modal"
opener={<button>Open Modal</button>}
onClose={() => console.log('Closed')}
>
<p>This is an accessible dialog!</p>
<button onClick={() => dialogRef.current?.close()}>Close</button>
</Dialog>
Observer
Tracks whether a child element is visible in the viewport using IntersectionObserver.
<Observer
onChange={(isVisible) => console.log('Visible:', isVisible)}
threshold={0.5}
>
<div>Watch me appear!</div>
</Observer>
LazyRender
Only renders children when they become visible in the viewport.
<LazyRender
placeholder={<span>Loading image...</span>}
threshold={0.1}
>
<img src="/heavy-image.jpg" alt="Lazy loaded" />
</LazyRender>
Hooks
AI Hooks (Chrome AI API)
Important: AI hooks require Chrome's experimental AI APIs. Always check availability with useAI first.
useAI
Check and manage availability of multiple Chrome AI APIs (Summarizer, Translator, LanguageDetector, and experimental APIs like Prompt, Writer, Rewriter, Proofreader).
import { useAI } from '@galiprandi/react-tools';
function MyComponent() {
// Check all APIs
const { isAvailable, apis, status, preload } = useAI();
// Check specific APIs only
const { isAvailable, apis } = useAI({
apis: ['translator', 'summarizer']
});
// Preload models for faster first use
useEffect(() => {
if (isAvailable) {
preload('translator');
}
}, [isAvailable, preload]);
// Show download progress
if (apis.translator.availability === 'downloading') {
const progress = getApiProgress('translator');
return <LoadingBar {...progress} />;
}
return <div>AI Status: {status}</div>;
}
Returns:
isAvailable: Whether all requested APIs are availablestatus:'idle' | 'loading' | 'ready' | 'error'apis: Status of each API (unavailable, downloadable, downloading, available)isApiAvailable(api): Check specific APIgetApiProgress(api): Get download progresspreload(api): Preload specific API modelpreloadAll(): Preload all APIs
useAISummarize
Generate text summaries with streaming support.
import { useAISummarize } from '@galiprandi/react-tools';
function Summarizer() {
const summarize = useAISummarize({
type: 'tldr',
format: 'markdown',
length: 'short',
outputLanguage: 'en',
streaming: true,
warmup: true
});
const handleSummarize = async () => {
await summarize.summarize(longText, 'End with: Powered by my app');
};
return (
<div>
<button onClick={handleSummarize}>Summarize</button>
{summarize.status === 'summarizing' && <p>Summarizing...</p>}
{summarize.data && <p>{summarize.data}</p>}
</div>
);
}
Options:
type:'tldr' | 'key-points' | 'teaser' | 'headline'format:'plain-text' | 'markdown'length:'short' | 'medium' | 'long'outputLanguage:'en' | 'es' | 'ja' | 'auto' | 'user'streaming: Enable real-time outputwarmup: Preload model on mount
useLanguageDetection
Detect language from text with confidence scores and user language comparison.
import { useLanguageDetection } from '@galiprandi/react-tools';
function LanguageDetector() {
const { lang, confidence, allLangs, userLang, isUserLang, status } =
useLanguageDetection({
text: 'Hallo und herzlich willkommen!',
minConfidence: 0.8,
maxResults: 3
});
return (
<div>
{status === 'detecting' && <p>Detecting...</p>}
{lang && (
<p>
Detected: {lang} ({Math.round(confidence! * 100)}% confidence)
{isUserLang && <span> (matches your language)</span>}
</p>
)}
{allLangs.length > 1 && (
<details>
<summary>All detected languages</summary>
<ul>
{allLangs.map(({ lang, confidence }) => (
<li key={lang}>{lang}: {Math.round(confidence * 100)}%</li>
))}
</ul>
</details>
)}
</div>
);
}
Options:
text: Text to detect language from (auto-detects when changed)enable: Enable/disable auto-detectionminConfidence: Minimum confidence threshold (0.0 - 1.0)maxResults: Maximum number of resultswarmup: Preload model on mount
useTranslator
Translate text between languages with auto-detection and browser language support. Supports 38+ languages.
import { useTranslator } from '@galiprandi/react-tools';
function Translator() {
// Auto-detect source, translate to browser language
const { data, detectedSourceLanguage, resolvedTargetLanguage, status } =
useTranslator({
text: 'Hello world, how are you?',
sourceLanguage: 'auto',
targetLanguage: 'user',
streaming: true
});
return (
<div>
{status === 'translating' && <p>Translating...</p>}
{data && (
<p>
{data}
<small>
(from {detectedSourceLanguage} to {resolvedTargetLanguage})
</small>
</p>
)}
</div>
);
}
Options:
text: Text to translate (auto-translates when changed)sourceLanguage:'auto'or specific language codetargetLanguage:'user'or specific language codestreaming: Enable real-time outputwarmup: Preload model on mountenable: Enable/disable auto-translation
Optimization: When source and target languages match, returns original text without loading translation model.
Utility Hooks
useDebounce
Debounce any value for search, filters, etc.
const debouncedSearch = useDebounce(searchTerm, 500);
useEffect(() => {
// API call only runs 500ms after searchTerm stops changing
if (debouncedSearch) {
fetch(`/api/search?q=${debouncedSearch}`);
}
}, [debouncedSearch]);
useTimer
Safe timer management with automatic cleanup.
const { setTimeout, setInterval, clearTimer, isActive } = useTimer({
onSetTimer: (id) => console.log('Timer set:', id),
onTimerComplete: (id) => console.log('Timer completed:', id),
onCancelTimer: (id) => console.log('Timer cancelled:', id),
});
// Set a timeout
setTimeout(() => console.log('Done!'), 5000);
// Set an interval
setInterval(() => console.log('Tick'), 1000);
// Clear any active timer
clearTimer();
useList
Simplify array state management with immutable helpers.
const { list, addItem, removeByIdx, updateByIdx, findItemBy, count } =
useList([{ id: 1, name: 'Item 1' }]);
// Add item
addItem({ id: 2, name: 'Item 2' });
// Remove by index
removeByIdx(0);
// Update by index
updateByIdx(0, (item) => ({ ...item, name: 'Updated' }));
// Find item
const item = findItemBy('id', 1);
// Count items
const total = count();
Utilities
dates
ISO to local datetime conversion (used internally by DateTime).
import { isoToLocal, localToIso } from '@galiprandi/react-tools/utilities/dates';
const localDate = isoToLocal('2024-01-15T10:30:00Z');
const isoDate = localToIso('2024-01-15T10:30');
strings
Value transforms for Input component.
import { toTitleCase, onlyEmail, snakeCase } from '@galiprandi/react-tools/utilities/strings';
toTitleCase('hello world'); // 'Hello World'
onlyEmail('test@example.com'); // 'test@example.com'
snakeCase('hello world'); // 'hello_world'
userLanguage
Browser language detection (used internally by useTranslator).
import { getUserLanguage } from '@galiprandi/react-tools/utilities/userLanguage';
const userLang = getUserLanguage(); // 'en', 'es', 'ja', etc.
Common Patterns
Pattern 1: Search with Debounce
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
const [results, setResults] = useState([]);
useEffect(() => {
if (debouncedSearch) {
fetch(`/api/search?q=${debouncedSearch}`)
.then(res => res.json())
.then(setResults);
}
}, [debouncedSearch]);
return (
<>
<Input
value={searchTerm}
onChangeValue={setSearchTerm}
placeholder="Search..."
/>
<AsyncBlock
promiseFn={() => fetch(`/api/search?q=${debouncedSearch}`).then(r => r.json())}
deps={[debouncedSearch]}
success={(data) => <ul>{data.map(item => <li>{item}</li>)}</ul>}
/>
</>
);
}
Pattern 2: Form with Validation
function UserForm() {
return (
<Form<{ email: string; username: string }>
onSubmitValues={(values) => {
// values are already transformed and filtered
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(values)
});
}}
filterEmptyValues
>
<Input
name="email"
label="Email"
transform="onlyEmail"
required
/>
<Input
name="username"
label="Username"
transform="titleCase"
required
/>
<button type="submit">Submit</button>
</Form>
);
}
Pattern 3: AI Pipeline
function AIPipeline() {
const ai = useAI({ apis: ['languageDetector', 'translator'] });
const { lang, confidence } = useLanguageDetection({ text: inputText });
const { data, status } = useTranslator({
text: inputText,
sourceLanguage: 'auto',
targetLanguage: 'user'
});
if (!ai.isAvailable) return <div>AI not available</div>;
return (
<div>
<p>Detected: {lang} ({Math.round(confidence! * 100)}%)</p>
{status === 'translating' && <p>Translating...</p>}
{data && <p>Translation: {data}</p>}
</div>
);
}
Pattern 4: Lazy Loading with Progress
function LazyImageGallery() {
const images = Array.from({ length: 50 }, (_, i) => ({
id: i,
src: `/images/image-${i}.jpg`
}));
return (
<div>
{images.map(img => (
<LazyRender
key={img.id}
placeholder={<div style={{ height: 300, background: '#eee' }}>Loading...</div>}
threshold={0.1}
>
<img src={img.src} alt={`Image ${img.id}`} loading="lazy" />
</LazyRender>
))}
</div>
);
}
Best Practices
AI Hooks
- Always check availability with
useAIbefore using AI hooks - Use
warmup: truefor faster first use in critical paths - Handle loading states and errors appropriately
- Use
streaming: truefor better UX with long-running operations
AsyncBlock
- Use
depsarray to re-execute when dependencies change - Set
timeOutto prevent hanging requests - Leverage automatic cancellation - no need for manual AbortSignal
- Use
onSuccess/onErrorfor side effects
Form/Input
- Use
transformprop for built-in validations (onlyEmail, titleCase, etc.) - Use
filterEmptyValueson Form to exclude empty fields - Leverage
onChangeDebouncefor search/filter inputs - Combine with
useDebouncefor form fields that trigger API calls
Performance
- Use
LazyRenderfor heavy images or components below the fold - Use
Observerfor scroll-based animations or lazy loading - Use
useDebouncefor search/filter inputs to reduce API calls - Use
useTimerinstead of directsetTimeout/setIntervalfor automatic cleanup
Browser Compatibility
- All components work in modern browsers
- AI hooks require Chrome with experimental AI APIs enabled
- Dialog uses native
<dialog>element (polyfill available for older browsers) - IntersectionObserver (Observer, LazyRender) requires modern browser support