Skip to content

Instantly share code, notes, and snippets.

@mfbx9da4
Last active May 24, 2021 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mfbx9da4/1d213c6f34c4ff92cc32ed8d28d2d05b to your computer and use it in GitHub Desktop.
Save mfbx9da4/1d213c6f34c4ff92cc32ed8d28d2d05b to your computer and use it in GitHub Desktop.
A wrapper around the local storage API which adds react hooks so that components can reactively update in response to storage changes.
import { createObservable as createObservableInner } from 'onin-shared/build/createObservable'
import { useEffect, useState } from 'react'
import { isMainThread } from './isMainThread'
export type Key = string
export type Value = string | undefined
const createObservable = (x: Value) => createObservableInner(x)
type Observable = ReturnType<typeof createObservable>
const Observables = {
dict: new Map<Key, Observable>(),
getOrCreate: (k: Key) => {
if (!Observables.dict.has(k)) Observables.dict.set(k, createObservable(get(k)))
return Observables.dict.get(k) as Observable
},
entries: () => Observables.dict.entries(),
}
export function get(key: Key): Value {
if (!isMainThread()) return
return localStorage.getItem(key) || undefined
}
export function set(key: Key, value: Value) {
if (!isMainThread()) return
if (value) {
localStorage.setItem(key, value)
} else {
localStorage.removeItem(key)
}
Observables.getOrCreate(key).set(value)
}
export function useKey(key: Key) {
const observable = Observables.getOrCreate(key)
const [val, setVal] = useState(observable.value)
useEffect(() => observable.subscribe(x => setVal(x)), [observable])
return val
}
export const clear = () => {
if (!isMainThread()) return
localStorage.clear()
refresh()
}
export const refresh = () => {
for (const [key, observable] of Observables.entries()) {
const val = get(key)
if (val !== observable.value) set(key, val)
}
}
const setupListener = () => {
if (!isMainThread()) return
// we listen to local storage incase another tab sets a key we are listening to
window.addEventListener('storage', () => refresh())
}
setupListener()
export type Listener<T> = (val: T) => void
export type Observable<T> = ReturnType<typeof createObservable>
export const createObservable = <T>(value: T) => {
const listeners: Set<Listener<T>> = new Set()
const subscribe = (x: Listener<T>) => {
x(value)
listeners.add(x)
return () => void listeners.delete(x)
}
const set = (val: T) => {
value = val
for (let x of listeners.values()) x(val)
}
return {
subscribe,
set,
get value() {
return value
},
}
}
export const isMainThread = () => {
return typeof window !== 'undefined' && typeof importScripts !== 'function'
}
function App () {
// Setting from a separate tab will cause this to fire also!
const token = useKey('my_token')
return <button onClick={() => set('my_token', 'asdf')}>Change my token</button>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment