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 content/docs/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Shadcn Hooks is a carefully curated collection of modern React hooks designed to
- [`useControllableValue`](/docs/hooks/use-controllable-value) - Manage a controllable value
- [`useCounter`](/docs/hooks/use-counter) - Create and manage counter state with increment, decrement, and reset
- [`useDebounce`](/docs/hooks/use-debounce) - A hook to debounce a value
- [`useLocalStorageState`](/docs/hooks/use-local-storage-state) - Persist state in localStorage with SSR-safe synchronization
- [`useResetState`](/docs/hooks/use-reset-state) - Reset a state to its initial state
- [`useThrottle`](/docs/hooks/use-throttle) - A hook to throttle a value
- [`useToggle`](/docs/hooks/use-toggle) - Simple boolean toggle functionality
Expand Down
1 change: 1 addition & 0 deletions skills/shadcn-hooks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ IMPORTANT: Each function entry includes a short `Description` and a detailed `Re
| [`useControllableValue`](references/useControllableValue.md) | Supports both controlled and uncontrolled component patterns | AUTO |
| [`useCounter`](references/useCounter.md) | Counter with `inc`, `dec`, `set`, `reset` helpers | AUTO |
| [`useDebounce`](references/useDebounce.md) | Debounced reactive value | AUTO |
| [`useLocalStorageState`](references/useLocalStorageState.md) | SSR-safe localStorage state with synchronization | AUTO |
| [`useResetState`](references/useResetState.md) | State with a `reset` function to restore the initial value | AUTO |
| [`useThrottle`](references/useThrottle.md) | Throttled reactive value | AUTO |
| [`useToggle`](references/useToggle.md) | Toggle between two values with utility actions | AUTO |
Expand Down
64 changes: 64 additions & 0 deletions skills/shadcn-hooks/references/useLocalStorageState.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# useLocalStorageState

Persist state in `localStorage` with SSR-safe snapshots and automatic same-tab / cross-tab synchronization.

## Usage

```tsx
import { useLocalStorageState } from '@/hooks/use-local-storage-state'

function Component() {
const [theme, setTheme, removeTheme] = useLocalStorageState('theme', 'light')

return (
<div>
<p>Theme: {theme}</p>
<button onClick={() => setTheme('dark')}>Dark</button>
<button
onClick={() => setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'))}
>
Toggle
</button>
<button onClick={removeTheme}>Reset</button>
</div>
)
}
```

## Type Declarations

```ts
import type { Dispatch, SetStateAction } from 'react'

export interface UseLocalStorageStateOptions<T> {
serializer?: (value: T) => string
deserializer?: (value: string) => T
onError?: (error: unknown) => void
}

export type UseLocalStorageStateReturn<T> = [
T,
Dispatch<SetStateAction<T>>,
() => void,
]

export function useLocalStorageState<T>(
key: string,
initialValue: T | (() => T),
options?: UseLocalStorageStateOptions<T>,
): UseLocalStorageStateReturn<T>
```

## Parameters

| Parameter | Type | Default | Description |
| -------------- | -------------------------------- | ------- | --------------------------------------------------------- |
| `key` | `string` | - | The `localStorage` key |
| `initialValue` | `T \| (() => T)` | - | Fallback value during SSR or when storage value is absent |
| `options` | `UseLocalStorageStateOptions<T>` | `{}` | Serializer, deserializer, and optional error callback |

## Returns

| Type | Description |
| -------------------------------- | ---------------------------------------------------- |
| `[value, setValue, removeValue]` | Current value, React-style updater, and clear method |
1 change: 1 addition & 0 deletions src/registry/hooks/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"use-controllable-value",
"use-counter",
"use-debounce",
"use-local-storage-state",
"use-reset-state",
"use-throttle",
"use-toggle",
Expand Down
75 changes: 75 additions & 0 deletions src/registry/hooks/use-hash/demo/demo-01.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client'
import { useState } from 'react'
import { Button } from '~/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '~/components/ui/card'
import { Input } from '~/components/ui/input'
import { useHash } from '..'

const PRESET_HASHES = ['intro', 'api', 'faq']

export function Demo01() {
const hash = useHash()
const [inputValue, setInputValue] = useState('demo-hash')

const setHash = (value: string) => {
const nextHash = value ? `#${value}` : ''
window.location.assign(
`${window.location.pathname}${window.location.search}${nextHash}`,
)
Comment on lines +20 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize user input before building the hash fragment.

If users type a leading #, Line 21 prepends another one and creates ##... in the URL fragment. Strip leading hashes (and whitespace) first.

Proposed fix
   const setHash = (value: string) => {
-    const nextHash = value ? `#${value}` : ''
+    const normalized = value.trim().replace(/^#+/, '')
+    const nextHash = normalized ? `#${normalized}` : ''
     window.location.assign(
       `${window.location.pathname}${window.location.search}${nextHash}`,
     )
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const setHash = (value: string) => {
const nextHash = value ? `#${value}` : ''
window.location.assign(
`${window.location.pathname}${window.location.search}${nextHash}`,
)
const setHash = (value: string) => {
const normalized = value.trim().replace(/^#+/, '')
const nextHash = normalized ? `#${normalized}` : ''
window.location.assign(
`${window.location.pathname}${window.location.search}${nextHash}`,
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/registry/hooks/use-hash/demo/demo-01.tsx` around lines 20 - 24, The
setHash function currently prepends a '#' without normalizing input; update
setHash to first trim whitespace and strip any leading '#' characters from the
incoming value (e.g., normalize = value.trim().replace(/^#+/, '')), then build
nextHash using the normalized string (nextHash = normalized ? `#${normalized}` :
'') and call window.location.assign as before; reference the setHash function to
apply this change.

}

return (
<Card className='shadow-none ring-0'>
<CardHeader>
<CardTitle>useHash Demo</CardTitle>
<CardDescription>
Update the URL hash and watch the hook value change in real time.
</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
<label htmlFor='hash-input' className='text-sm font-medium'>
Hash value
</label>
<Input
id='hash-input'
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder='Type hash without #'
/>

<div className='flex flex-wrap items-center gap-2'>
<Button type='button' onClick={() => setHash(inputValue)}>
Set hash
</Button>
<Button type='button' variant='outline' onClick={() => setHash('')}>
Clear hash
</Button>
</div>

<div className='flex flex-wrap items-center gap-2'>
{PRESET_HASHES.map((item) => (
<Button
key={item}
type='button'
variant='secondary'
onClick={() => setHash(item)}
>
#{item}
</Button>
))}
</div>

<p className='text-muted-foreground text-sm'>
Current hash:{' '}
<span className='text-foreground font-mono'>{hash}</span>
</p>
</CardContent>
</Card>
)
}
4 changes: 4 additions & 0 deletions src/registry/hooks/use-hash/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ title: useHash
description: A hook to get current hash
---

import { Demo01 } from './demo/demo-01'

<Demo01 />

## Installation

<Tabs items={['CLI', 'Manual']}>
Expand Down
42 changes: 42 additions & 0 deletions src/registry/hooks/use-local-storage-state/demo/demo-01.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client'
import { Button } from '~/components/ui/button'
import { Input } from '~/components/ui/input'
import { useLocalStorageState } from '..'

const DEMO_STORAGE_KEY = 'shadcn-hooks:demo:use-local-storage-state'

export function Demo01() {
const [name, setName, clearName] = useLocalStorageState<string>(
DEMO_STORAGE_KEY,
'',
)

return (
<div className='space-y-4'>
<div className='space-y-2'>
<label htmlFor='local-storage-name' className='text-sm font-medium'>
Persisted name
</label>
<Input
id='local-storage-name'
value={name}
onChange={(event) => setName(event.target.value)}
placeholder='Type and refresh the page'
/>
</div>

<div className='flex items-center gap-2'>
<Button type='button' variant='outline' onClick={() => setName('demo')}>
Fill sample
</Button>
<Button type='button' variant='destructive' onClick={clearName}>
Clear
</Button>
</div>

<p className='text-muted-foreground text-sm'>
Current value: <span className='font-mono'>{name || '(empty)'}</span>
</p>
</div>
)
}
56 changes: 56 additions & 0 deletions src/registry/hooks/use-local-storage-state/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: useLocalStorageState
description: A hook to persist state in localStorage with SSR-safe behavior
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix article in description text.

Line 3 should read “An SSR-safe...” instead of “A SSR-safe...”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/registry/hooks/use-local-storage-state/index.mdx` at line 3, Update the
description line in the MDX frontmatter for use-local-storage-state so it reads
"An SSR-safe behavior" instead of "A SSR-safe behavior" — locate the description
field in src/registry/hooks/use-local-storage-state/index.mdx and change the
leading article from "A" to "An".

---

import { Demo01 } from './demo/demo-01'

<Demo01 />

## Installation

<Tabs items={['CLI', 'Manual']}>
<Tab>
<InstallCLI value='use-local-storage-state' />
</Tab>
<Tab>
Copy and paste the following code into your project.
<RegistrySourceCode value='use-local-storage-state' />
</Tab>
</Tabs>

## API

```ts
import type { Dispatch, SetStateAction } from 'react'

export interface UseLocalStorageStateOptions<T> {
serializer?: (value: T) => string
deserializer?: (value: string) => T
onError?: (error: unknown) => void
}

export type UseLocalStorageStateReturn<T> = [
T,
Dispatch<SetStateAction<T>>,
() => void,
]

/**
* A SSR-safe localStorage state hook with same-tab and cross-tab synchronization.
*
* @param key - localStorage key
* @param initialValue - Initial state value, used during SSR and when key does not exist
* @param options - Optional serializer, deserializer, and error callback
* @returns [state, setState, removeState]
*/
export function useLocalStorageState<T>(
key: string,
initialValue: T | (() => T),
options?: UseLocalStorageStateOptions<T>,
): UseLocalStorageStateReturn<T>
```

## Credits

- [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)
Loading