Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
71 changes: 71 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,71 @@
'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'>
<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