Skip to content

Commit

Permalink
De-effectify some utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
glossawy committed Apr 18, 2024
1 parent 09187e0 commit c7e4fa2
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 59 deletions.
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import useLocalStorage from "@app/hooks/localStorage"
import { LocalStorageKeys } from "@app/utils/defaults"

function App() {
const [colorMode] = useLocalStorage(LocalStorageKeys.colorMode, "light")
const [colorMode] = useLocalStorage(LocalStorageKeys.colorMode, "system")

return (
<CssVarsProvider
Expand Down
40 changes: 17 additions & 23 deletions src/components/PopupAlertDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CloseRounded } from "@mui/icons-material"
import { Alert, IconButton, Stack, Typography } from "@mui/joy"
import { useEffect, useState } from "react"
import { useState } from "react"

import { AlertId, AlertType, useAlerts } from "@app/hooks/alerts"

Expand Down Expand Up @@ -31,34 +31,28 @@ export default function PopupAlertDisplay() {
commands: { clear },
} = useAlerts()

// Clear out used alerts
useEffect(() => {
const alertIds = new Set(alerts.map((a) => a.id))
const clearedIds = Array.from(alertTimeouts.keys()).filter(
(alertId) => !alertIds.has(alertId)
)

if (clearedIds.length === 0) return
const alertIds = new Set(alerts.map((a) => a.id))
const clearedIds = Array.from(alertTimeouts.keys()).filter(
(alertId) => !alertIds.has(alertId)
)

if (clearedIds.length > 0) {
const newAlertTimeouts = new Map(alertTimeouts)
clearedIds.forEach((alertId) => newAlertTimeouts.delete(alertId))
clearedIds.forEach((id) => newAlertTimeouts.delete(id))

setAlertTimeouts(newAlertTimeouts)
}, [alerts, alertTimeouts, setAlertTimeouts])
}

// Initiate timeouts for new alerts
useEffect(() => {
alerts.forEach((alert) => {
if (alertTimeouts.has(alert.id)) return
alerts.forEach((alert) => {
if (alertTimeouts.has(alert.id)) return

alertTimeouts.set(
alert.id,
setTimeout(() => {
clear(alert.id)
}, alert.durationMillis)
)
})
}, [alerts, alertTimeouts, clear])
alertTimeouts.set(
alert.id,
setTimeout(() => {
clear(alert.id)
}, alert.durationMillis)
)
})

const handleClose = (alertId: AlertId) => {
return () => clear(alertId)
Expand Down
9 changes: 9 additions & 0 deletions src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StorageUpdateEvent } from "@app/hooks/localStorage"

declare global {
interface WindowEventMap {
"rsseditor:storage-update": StorageUpdateEvent
}
}

export {}
11 changes: 3 additions & 8 deletions src/hooks/feedTransform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, useContext, useEffect } from "react"
import { createContext, useContext } from "react"

import useLocalStorage from "@app/hooks/localStorage"
import { LocalStorageKeys } from "@app/utils/defaults"
Expand Down Expand Up @@ -92,18 +92,13 @@ export const useFeedTransform = () => useContext(FeedTransformContext)
export const useFeedTransformDispatch = () =>
useContext(FeedTransformDispatchContext)

export const useStoredFeedTransform = (
export function useStoredFeedTransform(
initial: FeedTransform
): [FeedTransform, React.Dispatch<React.SetStateAction<FeedTransform>>] => {
): [FeedTransform, (newValue: FeedTransform) => void] {
const [storedFeedTransform, setStoredFeedTransform] = useLocalStorage(
LocalStorageKeys.feedTransform,
initial
)

// Runs migrations if needed exactly once on mount
useEffect(() => {
setStoredFeedTransform(migrate(storedFeedTransform))
}, []) // eslint-disable-line react-hooks/exhaustive-deps

return [migrate(storedFeedTransform), setStoredFeedTransform]
}
65 changes: 38 additions & 27 deletions src/hooks/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"
import { useCallback, useSyncExternalStore } from "react"

import { LocalStorageKeys } from "@app/utils/defaults"

Expand All @@ -11,38 +11,49 @@ export type Storable =
| Storable[]
| { [key: string]: Storable }

function coerce<T extends Storable>(
value: string,
{ fallback }: { fallback: T }
): T {
try {
return JSON.parse(value) as T
} catch (e) {
console.error(e)
return fallback
}
type KeyValue = (typeof LocalStorageKeys)[keyof typeof LocalStorageKeys]

type UpdateEventPayload = {
key: string
}

type KeyValue = (typeof LocalStorageKeys)[keyof typeof LocalStorageKeys]
export class StorageUpdateEvent extends CustomEvent<UpdateEventPayload> {
static type: keyof WindowEventMap = "rsseditor:storage-update" as const

constructor(payload: UpdateEventPayload) {
super(StorageUpdateEvent.type, { detail: payload })
}
}

export default function useLocalStorage<T extends Storable>(
key: KeyValue,
initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
const [value, setValue] = useState(() => {
if (!window.localStorage) return initialValue

const currentValue = localStorage.getItem(key)
return currentValue
? coerce<T>(currentValue, { fallback: initialValue })
: initialValue
})

useEffect(() => {
if (window.localStorage) {
): [T, (newValue: T) => void] {
const currentStoredItem = useSyncExternalStore(
(callback) => {
const listener = (evt: StorageUpdateEvent) => {
if (evt.detail.key === key) callback()
}

window.addEventListener("rsseditor:storage-update", listener)
return () => {
window.removeEventListener("rsseditor:storage-update", listener)
}
},
() => localStorage.getItem(key)
)

const setStoredValue = useCallback(
(value: T) => {
localStorage.setItem(key, JSON.stringify(value))
}
}, [key, value])
window.dispatchEvent(new StorageUpdateEvent({ key }))
},
[key]
)

const value: T = currentStoredItem
? JSON.parse(currentStoredItem)
: initialValue

return [value, setValue]
return [value, setStoredValue]
}

0 comments on commit c7e4fa2

Please sign in to comment.