OneApp Docs
PackagesCore

@repo/utils

Common utility functions for string manipulation, validation, formatting, and async operations. Merge Tailwind classes with cn(), format currency, validate emails, debounce functions. Tree-shakeable for small bundles.

Quick Start

Add utilities in 2 minutes:

pnpm add @repo/utils

Tree-shakeable, tested, ready to use. Skip to Quick Start →

Why @repo/utils?

Every package reimplements debounce differently. String utilities like truncate are duplicated across apps. Email validation regex differs by project (some accept invalid emails). Currency formatting is inconsistent. Tailwind class merging needs special handling (later classes override earlier ones). Date formatting uses different libraries.

@repo/utils solves this with shared, tested utilities for common tasks.

Production-ready with tree-shaking support, TypeScript types, comprehensive tests, and zero dependencies for core utilities.

Use cases

  • Tailwind class merging — Merge and override Tailwind classes correctly with cn()
  • String formatting — Truncate, slugify, capitalize, template strings
  • Date/currency formatting — Format dates, currency, numbers with locale support
  • Email/URL validation — Validate user input with regex patterns
  • Debounce/throttle — Performance optimization for search, scroll, resize

How it works

@repo/utils exports tree-shakeable utility functions:

import { cn, debounce, formatCurrency, isValidEmail } from "@repo/utils";

// Merge Tailwind classes (later classes override earlier)
const classes = cn("px-4 py-2", isActive && "bg-blue-500", isDisabled && "opacity-50");

// Debounce search input
const debouncedSearch = debounce((query: string) => {
  // API call
}, 300);

// Format currency with locale
formatCurrency(99.99, { currency: "USD" }); // "$99.99"

// Validate email
isValidEmail("user@example.com"); // true

Uses clsx for class merging, Intl API for formatting, tested regex for validation, and optimized async utilities.

Key features

Tailwind class mergingcn() utility handles conditional classes and overrides

String utilities — Truncate, slugify, capitalize, template strings

Date formatting — Format dates, relative time ("2 hours ago"), date math

Number/currency formatting — Locale-aware formatting with Intl API

Validation — Email, URL, empty checks with type guards

Async utilities — Debounce, throttle, sleep, retry with exponential backoff

Quick Start

1. Install the package

pnpm add @repo/utils

2. Merge Tailwind classes with cn()

components/Button.tsx
import { cn } from "@repo/utils";

interface ButtonProps {
  variant?: "primary" | "secondary";
  disabled?: boolean;
  className?: string;
}

export function Button({ variant = "primary", disabled, className }: ButtonProps) {
  return (
    <button
      className={cn(
        "px-4 py-2 rounded-md font-medium",
        variant === "primary" && "bg-blue-500 text-white",
        variant === "secondary" && "bg-gray-200 text-gray-900",
        disabled && "opacity-50 cursor-not-allowed",
        className // User classes override defaults
      )}
      disabled={disabled}
    >
      Click me
    </button>
  );
}

3. Format dates and currency

components/ProductCard.tsx
import { formatCurrency, formatDate, relativeTime } from "@repo/utils";

export function ProductCard({ product }: { product: Product }) {
  return (
    <div>
      <h3>{product.name}</h3>
      <p className="text-2xl font-bold">
        {formatCurrency(product.price, { currency: "USD" })}
      </p>
      <p className="text-sm text-gray-600">
        Listed {relativeTime(product.createdAt)}
      </p>

  );
}

4. Validate user input

components/NewsletterForm.tsx
"use client";

import { useState } from "react";
import { isValidEmail } from "@repo/utils";

export function NewsletterForm() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();

    if (!isValidEmail(email)) {
      setError("Please enter a valid email address");
      return;
    }

    // Submit email
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
      />
      {error && <p className="text-red-500">{error}</p>}
      <button type="submit">Subscribe</button>
    </form>
  );
}

That's it! You now have utilities for class merging, formatting, and validation across your app.

Debounce search and scroll handlers

Optimize performance with debounce and throttle:

import { debounce, throttle } from "@repo/utils";

// Debounce search (wait for user to stop typing)
const debouncedSearch = debounce((query: string) => {
  fetch(`/api/search?q=${query}`);
}, 300);

// Throttle scroll handler (run at most every 100ms)
const throttledScroll = throttle(() => {
  updateScrollPosition();
}, 100);

Technical Details

For Developers: Technical implementation details

Overview

PropertyValue
Locationpackages/utils
PurposeCommon utilities, helpers

Export Paths

PathDescription
@repo/utilsAll utilities
@repo/utils/stringString utilities
@repo/utils/arrayArray utilities
@repo/utils/objectObject utilities
@repo/utils/dateDate utilities
@repo/utils/validationValidation helpers

Class Name Utility

Tailwind Integration

The cn utility is optimized for Tailwind CSS and handles class merging correctly.

import { cn } from "@repo/utils";

// Merge class names
cn("px-4 py-2", "bg-blue-500");
// => "px-4 py-2 bg-blue-500"

// highlight-start
// Conditional classes
cn("base-class", isActive && "active", isDisabled && "disabled");
// => "base-class active" (if isActive is true)
// highlight-end

// Object syntax
cn({
  "text-red-500": hasError,
  "text-green-500": isSuccess
});

String Utilities

Truncate

import { truncate } from "@repo/utils/string";

truncate("This is a long string", 10);
// => "This is a..."

truncate("Short", 10);
// => "Short"

Slugify

import { slugify } from "@repo/utils/string";

slugify("Hello World!");
// => "hello-world"

slugify("Café & Restaurant");
// => "cafe-restaurant"

Capitalize

import { capitalize, capitalizeWords } from "@repo/utils/string";

capitalize("hello");
// => "Hello"

capitalizeWords("hello world");
// => "Hello World"

Template

import { template } from "@repo/utils/string";

template("Hello, {name}!", { name: "World" });
// => "Hello, World!"

Array Utilities

Chunk

import { chunk } from "@repo/utils/array";

chunk([1, 2, 3, 4, 5], 2);
// => [[1, 2], [3, 4], [5]]

Unique

import { unique, uniqueBy } from "@repo/utils/array";

unique([1, 2, 2, 3, 3, 3]);
// => [1, 2, 3]

uniqueBy(users, (u) => u.email);
// => unique users by email

Group By

import { groupBy } from "@repo/utils/array";

const posts = [
  { category: "tech", title: "Post 1" },
  { category: "life", title: "Post 2" },
  { category: "tech", title: "Post 3" }
];

groupBy(posts, (p) => p.category);
// => { tech: [...], life: [...] }

Shuffle

import { shuffle } from "@repo/utils/array";

shuffle([1, 2, 3, 4, 5]);
// => [3, 1, 5, 2, 4] (random order)

Object Utilities

Pick

import { pick } from "@repo/utils/object";

const user = { id: 1, name: "John", email: "john@example.com", password: "..." };

pick(user, ["id", "name"]);
// => { id: 1, name: "John" }

Omit

import { omit } from "@repo/utils/object";

omit(user, ["password"]);
// => { id: 1, name: "John", email: "john@example.com" }

Deep Clone

import { deepClone } from "@repo/utils/object";

const cloned = deepClone(complexObject);

Deep Merge

import { deepMerge } from "@repo/utils/object";

const merged = deepMerge(defaultConfig, userConfig);

Date Utilities

Format Date

import { formatDate } from "@repo/utils/date";

formatDate(new Date());
// => "January 15, 2024"

formatDate(new Date(), { format: "short" });
// => "Jan 15, 2024"

formatDate(new Date(), { format: "iso" });
// => "2024-01-15"

Relative Time

import { relativeTime } from "@repo/utils/date";

relativeTime(new Date(Date.now() - 3600000));
// => "1 hour ago"

relativeTime(new Date(Date.now() + 86400000));
// => "in 1 day"

Date Math

import { addDays, addMonths, startOfDay, endOfDay } from "@repo/utils/date";

addDays(new Date(), 7);
// => Date 7 days from now

startOfDay(new Date());
// => Date at 00:00:00

endOfDay(new Date());
// => Date at 23:59:59

Formatting

Format Currency

import { formatCurrency } from "@repo/utils";

formatCurrency(99.99);
// => "$99.99"

formatCurrency(99.99, { currency: "EUR", locale: "de-DE" });
// => "99,99 €"

Format Number

import { formatNumber } from "@repo/utils";

formatNumber(1234567);
// => "1,234,567"

formatNumber(1234567, { notation: "compact" });
// => "1.2M"

Format Bytes

import { formatBytes } from "@repo/utils";

formatBytes(1024);
// => "1 KB"

formatBytes(1536000);
// => "1.46 MB"

Validation

Input Validation

Always validate user input on both client and server. These utilities help ensure data integrity.

Email

import { isValidEmail } from "@repo/utils/validation";

// highlight-start
isValidEmail("user@example.com"); // true
isValidEmail("invalid-email"); // false
// highlight-end

URL

import { isValidUrl } from "@repo/utils/validation";

isValidUrl("https://example.com"); // true
isValidUrl("not-a-url"); // false

Empty Check

import { isEmpty, isNotEmpty } from "@repo/utils/validation";

isEmpty(""); // true
isEmpty([]); // true
isEmpty({}); // true
isEmpty(null); // true

isNotEmpty("hello"); // true
isNotEmpty([1, 2]); // true

Async Utilities

Debounce

import { debounce } from "@repo/utils";

const debouncedSearch = debounce((query: string) => {
  // Search logic
}, 300);

// Called multiple times, only executes after 300ms of no calls
debouncedSearch("hello");

Throttle

import { throttle } from "@repo/utils";

const throttledScroll = throttle(() => {
  // Scroll handler
}, 100);

// Called at most once every 100ms

Sleep

import { sleep } from "@repo/utils";

await sleep(1000); // Wait 1 second

Retry

import { retry } from "@repo/utils";

const result = await retry(() => fetchData(), { maxRetries: 3, delay: 1000 });

On this page