Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function Component() {
- [`useTernaryDarkMode`](https://usehooks-ts.com/react-hook/use-ternary-dark-mode) — manages ternary (system, dark, light) dark mode with local storage support.
- [`useTimeout`](https://usehooks-ts.com/react-hook/use-timeout) — handles timeouts in React components using the [setTimeout API](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout).
- [`useToggle`](https://usehooks-ts.com/react-hook/use-toggle) — manages a boolean toggle state in React components.
- [`useUniqueId`](https://usehooks-ts.com/react-hook/use-unique-id) — useUniqueId - A flexible, SSR-safe, secure hook for generating stable unique IDs
- [`useUnmount`](https://usehooks-ts.com/react-hook/use-unmount) — runs a cleanup function when the component is unmounted.
- [`useWindowSize`](https://usehooks-ts.com/react-hook/use-window-size) — tracks the size of the window.
<!-- HOOKS:END -->
Expand Down
1 change: 1 addition & 0 deletions packages/usehooks-ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function Component() {
- [`useTernaryDarkMode`](https://usehooks-ts.com/react-hook/use-ternary-dark-mode) — manages ternary (system, dark, light) dark mode with local storage support.
- [`useTimeout`](https://usehooks-ts.com/react-hook/use-timeout) — handles timeouts in React components using the [setTimeout API](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout).
- [`useToggle`](https://usehooks-ts.com/react-hook/use-toggle) — manages a boolean toggle state in React components.
- [`useUniqueId`](https://usehooks-ts.com/react-hook/use-unique-id) — useUniqueId - A flexible, SSR-safe, secure hook for generating stable unique IDs
- [`useUnmount`](https://usehooks-ts.com/react-hook/use-unmount) — runs a cleanup function when the component is unmounted.
- [`useWindowSize`](https://usehooks-ts.com/react-hook/use-window-size) — tracks the size of the window.
<!-- HOOKS:END -->
Expand Down
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export * from './useTimeout'
export * from './useToggle'
export * from './useUnmount'
export * from './useWindowSize'
export * from './useUniqueId'
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/useUniqueId/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useUniqueId'
30 changes: 30 additions & 0 deletions packages/usehooks-ts/src/useUniqueId/useUniqueId.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useUniqueId } from './useUniqueId'

// use in .tsx
function MyComponent() {
const id = useUniqueId();

return (
<div>
<label htmlFor={id}>Enter your name:</label>
<input id={id} type="text" />
</div>
);
}

// use in .ts
const id = useUniqueId();
// → "b1a9dba3bc934b6a84b1cc98b4feab1a"

const prefixedId = useUniqueId({ prefix: 'user-' });
// → "user-b1a9dba3bc934b6a84b1cc98b4feab1a"

const dashedId = useUniqueId({ withDashes: true });
// → "3cb742e6-96bb-4684-b9ea-7e46a5dfb324"

const shortIdWithPrefix = useUniqueId({ prefix: 'btn-', length: 10 });
// → "btn-f2e1cb42a1"

const shortId = useUniqueId({ length: 10 });
// "3fc7e2a9c1"

93 changes: 93 additions & 0 deletions packages/usehooks-ts/src/useUniqueId/useUniqueId.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# 🔐 useUniqueId

A **cryptographically secure**, **SSR-safe**, and **React-compliant** hook for generating stable, unique IDs per component instance effortlessly. Ideal for accessibility IDs, dynamic keys, DOM IDs, or any place where uniqueness and consistency are important.

- Accessibility attributes (like aria IDs)
- Dynamic React keys
- DOM element IDs
- Any scenario demanding consistent, unique identifiers across server and client renders

---

## 🛠 Why choose this hook?

**Cryptographically secure**: Utilizes the modern crypto.randomUUID() API when available for industry-standard randomness.

**Robust fallback strategy**: Gracefully falls back to crypto.getRandomValues() or Math.random() only if necessary, ensuring maximum compatibility.

**SSR-safe & React-friendly**: Guarantees stable IDs between server-side rendering and client hydration, preventing React reconciliation issues.

**Lightweight & deterministic**: No external dependencies, just pure, reliable uniqueness you can trust.

---

## 🔧 How it works

The hook generates a UUID string using:

- _crypto.randomUUID()_ — the most secure and standards-compliant method.
- _crypto.getRandomValues(_) — a secure fallback for environments without randomUUID.
- _Math.random()_ — as a last resort for legacy browsers or sandboxed iframes.
- This ensures maximum compatibility while maintaining security and uniqueness.
- If neither available (e.g., CSP-restricted or legacy browsers), uses Math.random()
- Ensures consistent ID generation per instance, even during SSR hydration

---

## 🚀 Features

- Generates **UUID v4–like** 32-character hex strings
- Supports **prefix**, **dashed format**, and **length truncation**
- Works with **React Server Components** and **SSR environments**
- **SSR-safe**: Ensures consistent IDs between server-side rendering and client hydration.
- **React-friendly**: Stable per component instance, preventing React reconciliation issues.
- **Robust fallback mechanism**: Uses crypto.randomUUID() if available, falls back to crypto.getRandomValues(), and finally Math.random() for legacy environments.
- **Lightweight & dependency-free**: Pure JavaScript with no external dependencies.
- Fallback to `Math.random()` when crypto APIs are unavailable
- Stable across re-renders — generated once per instance

---

## 📦 API

```ts
useUniqueId(options?: {
prefix?: string // Optional prefix string to prepend to ID
withDashes?: boolean // Include UUID dashes (default: false)
length?: number // Truncate output to this length (optional)
}): string
```

---

## 💡 Examples

```ts
const id = useUniqueId()
// → "b1a9dba3bc934b6a84b1cc98b4feab1a"

const prefixedId = useUniqueId({ prefix: 'user-' })
// → "user-b1a9dba3bc934b6a84b1cc98b4feab1a"

const dashedId = useUniqueId({ withDashes: true })
// → "3cb742e6-96bb-4684-b9ea-7e46a5dfb324"

const shortIdWithPrefix = useUniqueId({ prefix: 'btn-', length: 10 })
// → "btn-f2e1cb42a1"

const shortId = useUniqueId({ length: 10 })
// → "3fc7e2a9c1"
```

```js
function MyComponent() {
const id = useUniqueId()

return (
<div>
<label htmlFor={id}>Enter your name:</label>
<input id={id} type="text" />
</div>
)
}
```
92 changes: 92 additions & 0 deletions packages/usehooks-ts/src/useUniqueId/useUniqueId.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { renderHook } from '@testing-library/react'

import { useUniqueId } from './useUniqueId'

describe('useUniqueId()', () => {
const originalCrypto = globalThis.crypto

beforeEach(() => {
vi.resetAllMocks()
})

afterEach(() => {
Object.defineProperty(globalThis, 'crypto', {
value: originalCrypto,
configurable: true,
})
vi.restoreAllMocks()
})

it('returns a stable ID across renders', () => {
const { result, rerender } = renderHook(() => useUniqueId())
const first = result.current
rerender()
const second = result.current
expect(first).toBe(second)
})

it('generates a unique ID per instance', () => {
const first = renderHook(() => useUniqueId()).result.current
const second = renderHook(() => useUniqueId()).result.current
expect(first).not.toBe(second)
})

it('applies prefix correctly', () => {
const { result } = renderHook(() => useUniqueId({ prefix: 'user-' }))
expect(result.current.startsWith('user-')).toBe(true)
})

it('returns a UUID with dashes when withDashes is true', () => {
const { result } = renderHook(() => useUniqueId({ withDashes: true }))
const UUID_V4_REGEX =
/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i
expect(result.current).toMatch(UUID_V4_REGEX)
})

it('returns a UUID without dashes by default', () => {
const { result } = renderHook(() => useUniqueId())
expect(result.current.length).toBe(32)
expect(result.current.includes('-')).toBe(false)
})

it('respects the length option', () => {
const { result } = renderHook(() => useUniqueId({ length: 10 }))
expect(result.current.length).toBe(10)
})

it('handles length > 32 gracefully (no error)', () => {
const { result } = renderHook(() => useUniqueId({ length: 100 }))
expect(result.current.length).toBeGreaterThanOrEqual(32)
})

it('returns a fallback ID if crypto.randomUUID is missing', () => {
const originalCrypto = globalThis.crypto

const mockCrypto: Crypto = {
...originalCrypto,
getRandomValues: originalCrypto.getRandomValues,
// randomUUID is omitted to simulate unavailability
}

Object.defineProperty(globalThis, 'crypto', {
value: mockCrypto,
configurable: true,
writable: true,
})

const { result } = renderHook(() => useUniqueId())
expect(result.current.length).toBe(32)
expect(result.current.includes('-')).toBe(false)

// restore
globalThis.crypto = originalCrypto
})

it('does not break when crypto.getRandomValues throws', () => {
globalThis.crypto.getRandomValues = () => {
throw new Error('blocked by CSP')
}
const { result } = renderHook(() => useUniqueId())
expect(result.current.length).toBe(32)
})
})
100 changes: 100 additions & 0 deletions packages/usehooks-ts/src/useUniqueId/useUniqueId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useId, useRef } from 'react'

// Constants
const UUID_TEMPLATE = '10000000100040008000100000000000'
const BYTE_ARRAY_SIZE = 1
const BYTE_MAX = 256
const UUID_REPLACE_REGEX = /[018]/g
const DASH_REGEX = /-/g
const HEX_RADIX = 16
const BIT_MASK = 15
const VERSION_SHIFT_DIVISOR = 4

type UseUniqueIdOptions = {
prefix?: string
withDashes?: boolean
length?: number // Truncate if needed (e.g. 10 for nano-style)
}

/**
* Generates a cryptographically secure UUID (Universally Unique Identifier) v4–like string (32-character hex, no dashes).
* @returns {string} A 32-character lowercase hexadecimal UUID string (dashless).
*/

function generateSecureUUID(): string {
const cryptoObj =
typeof globalThis !== 'undefined' ? globalThis.crypto : undefined

try {
// Use native crypto.randomUUID if available (modern browsers)
if (cryptoObj?.randomUUID) {
return cryptoObj.randomUUID().replace(DASH_REGEX, '')
}

// Use getRandomValues fallback if available
const getRandomByte = cryptoObj?.getRandomValues
? () => {
const arr = new Uint8Array(BYTE_ARRAY_SIZE)
cryptoObj.getRandomValues(arr)
return arr[0]
}
: () => Math.floor(Math.random() * BYTE_MAX)

return UUID_TEMPLATE.replace(UUID_REPLACE_REGEX, (char: string) => {
const digit = Number(char)
const rand = getRandomByte() & BIT_MASK
const shifted = rand >> (digit / VERSION_SHIFT_DIVISOR)
return (digit ^ shifted).toString(HEX_RADIX)
})
} catch {
// Final fallback if crypto access fails (e.g., iframe security, CSP)
return UUID_TEMPLATE.replace(UUID_REPLACE_REGEX, (char: string) => {
const digit = Number(char)
const rand = Math.floor(Math.random() * BYTE_MAX) & BIT_MASK
const shifted = rand >> (digit / VERSION_SHIFT_DIVISOR)
return (digit ^ shifted).toString(HEX_RADIX)
})
}
}

/**
* UseUniqueId - A flexible, SSR-safe, secure hook for generating stable unique IDs.
* @param options - Optional config:
* - prefix: prepend to the ID
* - withDashes: return standard UUID format
* - length: truncate the ID to desired length.
* @returns Stable unique ID (string).
* @example
*/
export function useUniqueId(options?: UseUniqueIdOptions): string {
const { prefix = '', withDashes = false, length } = options || {}
const reactId = useId() // SSR-safe ID base
const idRef = useRef<string>()

if (!idRef.current) {
let baseId: string

if (typeof window === 'undefined') {
// On server, use React-generated ID
baseId = reactId.replace(/[:]/g, '')
} else {
baseId = generateSecureUUID()
}

if (
withDashes &&
typeof window !== 'undefined' &&
globalThis.crypto?.randomUUID
) {
baseId = globalThis.crypto.randomUUID() // Full dashed format
}

if (length) {
baseId = baseId.slice(0, length)
}

idRef.current = `${prefix}${baseId}`
}

return idRef.current
}