-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[utils] Port useLocalStorageState
hook from Toolpad
#41096
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
375e422
Port useLocalStorageState hook from Toolpad
Janpot 6c97043
theme switcher
Janpot 98a5523
cleaner
Janpot 8385be8
Update useLocalStorageState.ts
Janpot 420aee2
Update useLocalStorageState.ts
Janpot 1d0f58b
some extra mem
Janpot 0893acb
Merge branch 'heads/upstream/master' into use-local-storage-state
Janpot 8bff4db
Update package.json
Janpot b41efdc
Merge branch 'master' into use-local-storage-state
Janpot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './useLocalStorageState'; |
155 changes: 155 additions & 0 deletions
155
packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
'use client'; | ||
|
||
import * as React from 'react'; | ||
|
||
const NOOP = () => {}; | ||
|
||
// storage events only work across tabs, we'll use an event emitter to announce within the current tab | ||
const currentTabChangeListeners = new Map<string, Set<() => void>>(); | ||
|
||
function onCurrentTabStorageChange(key: string, handler: () => void) { | ||
let listeners = currentTabChangeListeners.get(key); | ||
|
||
if (!listeners) { | ||
listeners = new Set(); | ||
currentTabChangeListeners.set(key, listeners); | ||
} | ||
|
||
listeners.add(handler); | ||
} | ||
|
||
function offCurrentTabStorageChange(key: string, handler: () => void) { | ||
const listeners = currentTabChangeListeners.get(key); | ||
if (!listeners) { | ||
return; | ||
} | ||
|
||
listeners.delete(handler); | ||
|
||
if (listeners.size === 0) { | ||
currentTabChangeListeners.delete(key); | ||
} | ||
} | ||
|
||
function emitCurrentTabStorageChange(key: string) { | ||
const listeners = currentTabChangeListeners.get(key); | ||
if (listeners) { | ||
listeners.forEach((listener) => listener()); | ||
} | ||
} | ||
|
||
function subscribe(area: Storage, key: string, cb: () => void): () => void { | ||
const storageHandler = (event: StorageEvent) => { | ||
if (event.storageArea === area && event.key === key) { | ||
cb(); | ||
} | ||
}; | ||
window.addEventListener('storage', storageHandler); | ||
onCurrentTabStorageChange(key, cb); | ||
return () => { | ||
window.removeEventListener('storage', storageHandler); | ||
offCurrentTabStorageChange(key, cb); | ||
}; | ||
} | ||
|
||
function getSnapshot(area: Storage, key: string): string | null { | ||
return area.getItem(key); | ||
} | ||
|
||
function setValue(area: Storage, key: string, value: string | null) { | ||
if (typeof window !== 'undefined') { | ||
if (value === null) { | ||
area.removeItem(key); | ||
} else { | ||
area.setItem(key, String(value)); | ||
} | ||
emitCurrentTabStorageChange(key); | ||
} | ||
} | ||
|
||
type Initializer<T> = () => T; | ||
|
||
type UseStorageStateHookResult<T> = [T, React.Dispatch<React.SetStateAction<T>>]; | ||
|
||
function useLocalStorageStateServer( | ||
key: string | null, | ||
initializer: string | Initializer<string>, | ||
): UseStorageStateHookResult<string>; | ||
function useLocalStorageStateServer( | ||
key: string | null, | ||
initializer?: string | null | Initializer<string | null>, | ||
): UseStorageStateHookResult<string | null>; | ||
function useLocalStorageStateServer( | ||
key: string | null, | ||
initializer: string | null | Initializer<string | null> = null, | ||
): UseStorageStateHookResult<string | null> | UseStorageStateHookResult<string> { | ||
const [initialValue] = React.useState(initializer); | ||
return [initialValue, () => {}]; | ||
} | ||
|
||
/** | ||
* Sync state to local storage so that it persists through a page refresh. Usage is | ||
* similar to useState except we pass in a storage key so that we can default | ||
* to that value on page load instead of the specified initial value. | ||
* | ||
* Since the storage API isn't available in server-rendering environments, we | ||
* return initialValue during SSR and hydration. | ||
* | ||
* Things this hook does different from existing solutions: | ||
* - SSR-capable: it shows initial value during SSR and hydration, but immediately | ||
* initializes when clientside mounted. | ||
* - Sync state across tabs: When another tab changes the value in the storage area, the | ||
* current tab follows suit. | ||
*/ | ||
function useLocalStorageStateBrowser( | ||
key: string | null, | ||
initializer: string | Initializer<string>, | ||
): UseStorageStateHookResult<string>; | ||
function useLocalStorageStateBrowser( | ||
key: string | null, | ||
initializer?: string | null | Initializer<string | null>, | ||
): UseStorageStateHookResult<string | null>; | ||
function useLocalStorageStateBrowser( | ||
key: string | null, | ||
initializer: string | null | Initializer<string | null> = null, | ||
): UseStorageStateHookResult<string | null> | UseStorageStateHookResult<string> { | ||
const [initialValue] = React.useState(initializer); | ||
const area = window.localStorage; | ||
const subscribeKey = React.useCallback( | ||
(cb: () => void) => (key ? subscribe(area, key, cb) : NOOP), | ||
[area, key], | ||
); | ||
const getKeySnapshot = React.useCallback( | ||
() => (key && getSnapshot(area, key)) ?? initialValue, | ||
[area, initialValue, key], | ||
); | ||
const getKeyServerSnapshot = React.useCallback(() => initialValue, [initialValue]); | ||
|
||
const storedValue = React.useSyncExternalStore( | ||
subscribeKey, | ||
getKeySnapshot, | ||
getKeyServerSnapshot, | ||
); | ||
|
||
const setStoredValue = React.useCallback( | ||
(value: React.SetStateAction<string | null>) => { | ||
if (key) { | ||
const valueToStore = value instanceof Function ? value(storedValue) : value; | ||
setValue(area, key, valueToStore); | ||
} | ||
}, | ||
[area, key, storedValue], | ||
); | ||
|
||
const [nonStoredValue, setNonStoredValue] = React.useState(initialValue); | ||
|
||
if (!key) { | ||
return [nonStoredValue, setNonStoredValue]; | ||
} | ||
|
||
return [storedValue, setStoredValue]; | ||
} | ||
|
||
export default typeof window === 'undefined' | ||
? useLocalStorageStateServer | ||
: useLocalStorageStateBrowser; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This comment was marked as off-topic.
Sorry, something went wrong.