diff --git a/libs/hooks/src/lib/index.ts b/libs/hooks/src/lib/index.ts index c64ca6a..f627d02 100644 --- a/libs/hooks/src/lib/index.ts +++ b/libs/hooks/src/lib/index.ts @@ -3,8 +3,10 @@ export * from './useCombinedControlState' export * from './useCombineEventHandlers' export * from './useCombineRefs' export * from './useEvent' +export * from './useGlobalObject' export * from './useIsBrowser' export * from './useIsomorphicLayoutEffect' export * from './useMountEffect' export * from './useResizeObserver' +export * from './useStorageState' export * from './useUnmountEffect' diff --git a/libs/hooks/src/lib/useGlobalObject/index.ts b/libs/hooks/src/lib/useGlobalObject/index.ts new file mode 100644 index 0000000..43aa20e --- /dev/null +++ b/libs/hooks/src/lib/useGlobalObject/index.ts @@ -0,0 +1 @@ +export * from './useGlobalObject' diff --git a/libs/hooks/src/lib/useGlobalObject/useGlobalObject.ts b/libs/hooks/src/lib/useGlobalObject/useGlobalObject.ts new file mode 100644 index 0000000..ac2a296 --- /dev/null +++ b/libs/hooks/src/lib/useGlobalObject/useGlobalObject.ts @@ -0,0 +1,19 @@ +const useGlobalObject = () => { + if (typeof window !== 'undefined') { + return window + } + + if (typeof global !== 'undefined') { + return global + } + + if (typeof globalThis !== 'undefined') { + return globalThis + } + + throw new Error( + 'You are running in an unsupported environment. Make sure your environment has a global object present. More info: https://developer.mozilla.org/en-US/docs/Glossary/Global_object' + ) +} + +export { useGlobalObject } diff --git a/libs/hooks/src/lib/useStorageState/index.ts b/libs/hooks/src/lib/useStorageState/index.ts new file mode 100644 index 0000000..69d202c --- /dev/null +++ b/libs/hooks/src/lib/useStorageState/index.ts @@ -0,0 +1,2 @@ +export { useLocalStorageDb, useSessionStorageDb } from './useStorageDb' +export { useLocalStorageState, useSessionStorageState } from './useStorageState' diff --git a/libs/hooks/src/lib/useStorageState/useStorageDb.ts b/libs/hooks/src/lib/useStorageState/useStorageDb.ts new file mode 100644 index 0000000..ea9fb07 --- /dev/null +++ b/libs/hooks/src/lib/useStorageState/useStorageDb.ts @@ -0,0 +1,84 @@ +import type { SetStateAction } from 'react' +import { useMemo } from 'react' + +import { useEvent } from '../useEvent' +import { useGlobalObject } from '../useGlobalObject' +import { useStorageState } from './useStorageState' + +type UseStorageDb = Record> + +type UseStorageDbPrimaryKey = string | number + +type UseStorageDbRecord = { + id: UseStorageDbPrimaryKey + [property: string]: unknown +} + +type Params = { + id: UseStorageDbPrimaryKey + payload: UseStorageDbRecord +} + +const useStorageDb = (key: string, storage: Storage) => { + const [value, setValue] = useStorageState(key, storage) + + const db: Record> = useMemo(() => { + return value ? JSON.parse(value) : {} + }, [value]) + + const setDb = useEvent((valueOrSetter: SetStateAction) => { + const newValue = typeof valueOrSetter === 'function' ? valueOrSetter(db) : valueOrSetter + + setValue(JSON.stringify(newValue)) + }) + + return useMemo( + () => ({ + createOne: (resource: string, params: Params) => { + setDb(current => ({ + ...current, + [resource]: [ + ...(current[resource] || []), + { + ...params.payload + } + ] + })) + }, + getList: (resource: string, params: Params) => db[resource] || [], + getOne: (resource: string, params: Params) => + db[resource]?.find((item: UseStorageDbRecord) => item.id === params.id), + updateOne: (resource: string, params: Params) => { + setDb(current => ({ + ...current, + [resource]: (current[resource] || []).map((itemToUpdate: UseStorageDbRecord) => { + if (itemToUpdate.id === params.id) { + return { + ...itemToUpdate, + ...params.payload, + id: itemToUpdate.id + } + } + + return itemToUpdate + }) + })) + }, + deleteOne: (resource: string, params: Params) => { + setDb(current => ({ + ...current, + [resource]: (current[resource] || []).filter((itemToUpdate: UseStorageDbRecord) => { + return itemToUpdate.id !== params.id + }) + })) + } + }), + [db, setDb] + ) +} + +const useLocalStorageDb = (key: string) => useStorageDb(key, useGlobalObject().localStorage) + +const useSessionStorageDb = (key: string) => useStorageDb(key, useGlobalObject().sessionStorage) + +export { useLocalStorageDb, useSessionStorageDb, useStorageDb } diff --git a/libs/hooks/src/lib/useStorageState/useStorageState.ts b/libs/hooks/src/lib/useStorageState/useStorageState.ts new file mode 100644 index 0000000..87caff2 --- /dev/null +++ b/libs/hooks/src/lib/useStorageState/useStorageState.ts @@ -0,0 +1,39 @@ +import { Dispatch, SetStateAction, useEffect } from 'react' +import { useState } from 'react' + +import { useEvent } from '../useEvent' +import { useGlobalObject } from '../useGlobalObject' + +const useStorageState = (key: string, storage: Storage): [string | null, Dispatch>] => { + const [value, setValue] = useState(() => { + if (typeof storage === 'undefined') { + return null + } + + return storage.getItem(key) + }) + + const setStorageValue = useEvent((valueOrSetter: SetStateAction) => { + setValue(valueOrSetter) + + const newValue = typeof valueOrSetter === 'function' ? valueOrSetter(value) : valueOrSetter + + if (newValue === null) { + storage.removeItem(key) + } else { + storage.setItem(key, newValue) + } + }) + + useEffect(() => { + setValue(storage.getItem(key)) + }, [key, storage]) + + return [value, setStorageValue] +} + +const useLocalStorageState = (key: string) => useStorageState(key, useGlobalObject().localStorage) + +const useSessionStorageState = (key: string) => useStorageState(key, useGlobalObject().sessionStorage) + +export { useLocalStorageState, useSessionStorageState, useStorageState }