Utils & Extensions (@numa/utils)
Pure helper functions, prototype extensions, and shared React hooks.
Package structure
packages/utils/
├── cn.ts # clsx + tailwind-merge
├── cookies.ts # Cookie get/set/remove helpers
├── locale.ts # Locale resolution helpers
├── validation.ts # Phone, JWT validation
├── logger.ts # Structured console logger
├── filters.ts # Filtering utilities
├── form-diff.ts # Form diff utilities
├── icon.tsx # Icon helper
├── typography.tsx # Typography helper
├── extensions/ # Prototype extensions
│ ├── index.ts # Re-exports all extensions
│ ├── string.ts # String.prototype extensions
│ ├── number.ts # Number.prototype extensions
│ ├── date.ts # Date/string date formatting
│ └── duration.ts # Duration formatting
└── hooks/ # Shared React hooks
├── use-delayed-show.ts # Deferred UI (e.g. spinner after delay)
└── use-page-filters.ts # Page filter state managementImports
import { cn } from "@numa/utils/cn";
import { getCookie } from "@numa/utils/cookies";
import { useDelayedShow } from "@numa/utils/hooks";
import "@numa/utils/extensions";Utils vs Extensions
| Type | Location | Use Case |
|---|---|---|
| Utils | Direct imports | Pure functions you import and call |
| Extensions | extensions/ | Prototype methods on String, Number |
Prefer extensions for natural chaining like "2024-01-01".toDate(). Use utils for one-off helpers.
cn() — Class merging
Combines clsx and tailwind-merge for conditional Tailwind classes:
import { cn } from "@numa/utils/cn";
<div className={cn("base-class", condition && "conditional-class")} />Cookie helpers
import { getCookie, setCookie, removeCookie } from "@numa/utils/cookies";
const token = getCookie("_numa_access_token_");
setCookie("_numa_selected_locale_", "en");
removeCookie("_numa_access_token_");Logger
Structured console logger (only file with no-console ESLint disable):
import { logger } from "@numa/utils/logger";
logger.info("User logged in", { userId: "123" });
logger.error("Failed to fetch", error);Extensions
Extensions must be imported once in the app entry point for their prototype augmentations to apply:
import "@numa/utils/extensions";This is done in @numa/providers/rq.tsx.
String extensions
| Method | Description | Example |
|---|---|---|
sanitize() | Trim, lowercase, strip spaces | " Hello ".sanitize() → "hello" |
toShort(length?) | Truncate with ellipsis | "Long text".toShort(4) → "Long..." |
Number extensions
| Method | Description | Example |
|---|---|---|
toNumber() | Locale-formatted string | 1234.toNumber() → "1,234" |
toMoney() | Formatted with currency (Rwf) | 5000.toMoney() → "Rwf 5,000" |
toKilo() | Compact format | 1500.toKilo() → "1.5K" |
toUnit(unit) | Number with unit suffix | 5.toUnit("KM") → "5 KMs" |
Date extensions
Available on String:
| Method | Description |
|---|---|
toDate() | Formatted date |
toDateOnly() | Date without time |
toDateAndTime() | Date with time |
toTextDMY() | Text date (Day Month Year) |
toDuration(endTime) | Duration between dates |
Hooks
useDelayedShow
Defers showing a UI element (e.g., spinner) by a configurable delay:
const show = useDelayedShow(isLoading, 300);
return show ? <Spinner /> : null;usePageFilters
Manages filter state for paginated list pages:
const { filters, updateFilter, resetFilters } = usePageFilters({
search: "",
status: "active",
});Last updated on