diff --git a/docs/src/pages/_meta.json b/docs/src/pages/_meta.json index 44ae3551..d527103c 100644 --- a/docs/src/pages/_meta.json +++ b/docs/src/pages/_meta.json @@ -70,6 +70,8 @@ "title": "Utilities" }, "context-state": {}, + "context-local-storage-state": {}, + "context-session-storage-state": {}, "create-fixed-array": {}, "invariant-nullthrow": {}, "noop": {}, diff --git a/docs/src/pages/create-local-storage-state.mdx b/docs/src/pages/create-local-storage-state.mdx new file mode 100644 index 00000000..36c59b26 --- /dev/null +++ b/docs/src/pages/create-local-storage-state.mdx @@ -0,0 +1,69 @@ +--- +title: Create localStorage State +--- + +# Create localStorage State + +import ExportMetaInfo from '../components/export-meta-info'; + + + +Store your shared state that lives in localStorage and synchronize the state of a component with the data stored in the `localStorage`. Lift your state up and passing them deeply into your React app without worrying about performance. + +## Usage + +First, create a shared hooks with `createLocalStorageState`. It is recommended to place them in a separate file: + +```tsx filename="src/context/sidebar-active.tsx" copy +import { createLocalStorageState } from 'foxact/create-local-storage-state'; + +const [useSidebarActive, useSidebarActiveValue] = createLocalStorageState( + 'sidebar-active', // The localStorage key + /** + * The initial value to use if there is no item in the local storage with the provided key, + * the undefined value will be used if no initial value is provided. + * + * Also, the initial value will also be used during the server-side rendering, see below. + */ + false, + /** + * Optional configuration object enables the customization of value serialization before it's stored in local storage. + */ + { + // Optional, default to false. When set to "true", the value will be passed to the localStorage API as is. + raw: false, + // Optional, default to "JSON.stringify". Can only be specified when the "raw" is set to false (which is the default). + serializer: JSON.stringify, + // Optional, default to "JSON.parse". Can only be specified when the "raw" is set to false (which is the default). + deserializer: JSON.parse, + } +); + +export { useSidebarActive, useSidebarActiveValue }; +``` + +And now you can use the getter and setter hooks anywhere in your app: + +```tsx filename="src/components/sidebar.tsx" copy +import { memo } from 'react'; +import { useSidebarActive, useSidebarActiveValue } from '../context/sidebar-active'; + +function Sidebar() { + const [sidebarActive, setSidebarActive] = useSidebarActive(); + // If you only need the value, you can use `useSidebarActiveValue` instead: + const sidebarActive = useSidebarActiveValue(); + + return ( +
+ +
+ ); +} + +export default memo(Sidebar); +``` + +## Sever-side Rendering + +If the second argument (the initial value) is provided, React will use the initial value to render the UI and generate HTML on the server, otherwise React will find the closest `` boundary and render its `fallback` UI into the generated server HTML on the server. See +[`useLocalStorage`](/use-local-storage#sever-side-rendering) for more information. diff --git a/docs/src/pages/create-session-storage-state.mdx b/docs/src/pages/create-session-storage-state.mdx new file mode 100644 index 00000000..ec71b44c --- /dev/null +++ b/docs/src/pages/create-session-storage-state.mdx @@ -0,0 +1,66 @@ +--- +title: Create sessionStorage State +--- + +# Create sessionStorage State + +import ExportMetaInfo from '../components/export-meta-info'; + + + +Store your shared state that lives in sessionStorage and synchronize the state of a component with the data stored in the `localStorage`. Lift your state up and passing them deeply into your React app without worrying about performance. + +## Usage + +```tsx filename="src/context/sidebar-active.tsx" copy +import { createSessionStorageState } from 'foxact/create-session-storage-state'; + +const [useSidebarActive, useSidebarActiveValue] = createSessionStorageState( + 'sidebar-active', // The localStorage key + /** + * The initial value to use if there is no item in the local storage with the provided key, + * the undefined value will be used if no initial value is provided. + * + * Also, the initial value will also be used during the server-side rendering, see below. + */ + false, + /** + * Optional configuration object enables the customization of value serialization before it's stored in local storage. + */ + { + // Optional, default to false. When set to "true", the value will be passed to the localStorage API as is. + raw: false, + // Optional, default to "JSON.stringify". Can only be specified when the "raw" is set to false (which is the default). + serializer: JSON.stringify, + // Optional, default to "JSON.parse". Can only be specified when the "raw" is set to false (which is the default). + deserializer: JSON.parse, + } +); + +export { useSidebarActive, useSidebarActiveValue }; +``` + +And now you can use the getter and setter hooks anywhere in your app: + +```tsx filename="src/components/sidebar.tsx" copy +import { memo } from 'react'; +import { useSidebarActive, useSidebarActiveValue } from '../context/sidebar-active'; + +function Sidebar() { + const [sidebarActive, setSidebarActive] = useSidebarActive(); + // If you only need the value, you can use `useSidebarActiveValue` instead: + const sidebarActive = useSidebarActiveValue(); + + return ( +
+ +
+ ); +} + +export default memo(Sidebar); +``` + +## Sever-side Rendering + +See [createLocalStorageState](/create-local-storage-state#sever-side-rendering) for more details. diff --git a/src/create-local-storage-state/index.ts b/src/create-local-storage-state/index.ts new file mode 100644 index 00000000..ef220c82 --- /dev/null +++ b/src/create-local-storage-state/index.ts @@ -0,0 +1,20 @@ +import 'client-only'; + +import { createStorageStateFactory } from '../create-storage-state-factory'; + +/** + * @see https://foxact.skk.moe/create-local-storage-state + * + * @example + * ```ts + * const [useOpenState, useOpen] = createLocalStorageState( + * 'open', // storage key + * false, // server default value + * { raw: false } // options + * ); + * + * const [open, setOpen] = useOpenState(); + * const open = useOpen(); + * ``` + */ +export const createLocalStorageState = createStorageStateFactory('localStorage'); diff --git a/src/create-session-storage-state/index.ts b/src/create-session-storage-state/index.ts new file mode 100644 index 00000000..f936920c --- /dev/null +++ b/src/create-session-storage-state/index.ts @@ -0,0 +1,21 @@ +import 'client-only'; + +import { createStorageStateFactory } from '../create-storage-state-factory'; + +/** + * @see https://foxact.skk.moe/create-session-storage-state + * + * @example + * ```ts + * ```ts + * const [useOpenState, useOpen] = createSessionStorageState( + * 'open', // storage key + * false, // server default value + * { raw: false } // options + * ); + * + * const [open, setOpen] = useOpenState(); + * const open = useOpen(); + * ``` + */ +export const createSessionStorageState = createStorageStateFactory('sessionStorage'); diff --git a/src/create-storage-hook/index.ts b/src/create-storage-hook/index.ts index 3a14e5f2..1bf6fc69 100644 --- a/src/create-storage-hook/index.ts +++ b/src/create-storage-hook/index.ts @@ -4,8 +4,10 @@ import { noop } from '../noop'; import { useLayoutEffect } from '../use-isomorphic-layout-effect'; import { noSSRError } from '../no-ssr'; -type StorageType = 'localStorage' | 'sessionStorage'; -type NotUndefined = T extends undefined ? never : T; +export type StorageType = 'localStorage' | 'sessionStorage'; +export type NotUndefined = T extends undefined ? never : T; + +export type StateHookTuple = readonly [T, React.Dispatch>]; // StorageEvent is deliberately not fired on the same document, we do not want to change that type CustomStorageEvent = CustomEvent; @@ -105,13 +107,13 @@ export function createStorage(type: StorageType) { key: string, serverValue: NotUndefined, options?: UseStorageRawOption | UseStorageParserOption - ): readonly [T, React.Dispatch>]; + ): StateHookTuple; // client-render only function useStorage( key: string, serverValue?: undefined, options?: UseStorageRawOption | UseStorageParserOption - ): readonly [T | null, React.Dispatch>]; + ): StateHookTuple; function useStorage( key: string, serverValue?: NotUndefined, @@ -121,7 +123,7 @@ export function createStorage(type: StorageType) { serializer: JSON.stringify, deserializer: JSON.parse } - ): readonly [T | null, React.Dispatch>] | readonly [T, React.Dispatch>] { + ): StateHookTuple | StateHookTuple { const subscribeToSpecificKeyOfLocalStorage = useCallback((callback: () => void) => { if (typeof window === 'undefined') { return noop; diff --git a/src/create-storage-state-factory/index.ts b/src/create-storage-state-factory/index.ts new file mode 100644 index 00000000..8af2b73f --- /dev/null +++ b/src/create-storage-state-factory/index.ts @@ -0,0 +1,42 @@ +import { createStorage } from '../create-storage-hook'; +import type { NotUndefined, StateHookTuple, StorageType, UseStorageParserOption, UseStorageRawOption } from '../create-storage-hook'; + +const identity = (x: any) => x; + +export type ValueHook = () => T; +export type SetValueHook = () => (value: T) => void; +export type StateHook = () => StateHookTuple; + +export function createStorageStateFactory(type: StorageType) { + function createStorageState( + key: string, + serverValue: NotUndefined, + options?: UseStorageRawOption | UseStorageParserOption + ): readonly [StateHook, ValueHook, SetValueHook]; + function createStorageState( + key: string, + serverValue?: undefined, + options?: UseStorageRawOption | UseStorageParserOption + ): readonly [StateHook, ValueHook, SetValueHook]; + function createStorageState( + key: string, + serverValue?: NotUndefined, + // eslint-disable-next-line sukka/unicorn/no-object-as-default-parameter -- two different shape of options + options: UseStorageRawOption | UseStorageParserOption = { + raw: false, + serializer: JSON.stringify, + deserializer: JSON.parse + } + ): readonly [StateHook, ValueHook, SetValueHook] | readonly [StateHook, ValueHook, SetValueHook] { + const { useStorage: useStorageOriginal, useSetStorage: useSetStorageOriginal } = createStorage(type); + + const useStorage = () => useStorageOriginal(key, serverValue as any, options); + const useStorageState = () => useStorageOriginal(key, serverValue as any, options)[0]; + + const useSetStorageValue = () => useSetStorageOriginal(key, options.raw ? identity : options.serializer); + + return [useStorage, useStorageState, useSetStorageValue]; + }; + + return createStorageState; +}