From 80868ee44130a34c8383a0f4184b83b92ead78a0 Mon Sep 17 00:00:00 2001 From: Ridwan Olanrewaju Date: Wed, 22 Nov 2023 08:07:13 +0100 Subject: [PATCH] Make usePortal initial state either a state or function of the state. --- src/addons/withImplementation.ts | 2 +- src/component/usePortal.ts | 10 +++++----- src/definition/cookie.ts | 5 ++--- src/definition/portal.ts | 25 +++++++++++++++++-------- src/subject/behaviorSubject.ts | 25 ++++++++++--------------- src/subject/portal.ts | 4 +--- src/utilities/getComputedState.ts | 14 +++++++------- src/utilities/getResolvedState.ts | 14 ++++++++++++++ src/utilities/index.ts | 1 + src/utilities/isSetStateFunction.ts | 4 ++-- src/utilities/objectToStringKey.ts | 24 ++++++++---------------- 11 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 src/utilities/getResolvedState.ts diff --git a/src/addons/withImplementation.ts b/src/addons/withImplementation.ts index 28b2157..9f9e103 100644 --- a/src/addons/withImplementation.ts +++ b/src/addons/withImplementation.ts @@ -88,7 +88,7 @@ export function usePortalImplementation< /** * Return an array containing the current state and the setter function for state updates. - * @type {PortalState} + * @type {PortalState} */ return [select(observable.value), observable.setter]; } diff --git a/src/component/usePortal.ts b/src/component/usePortal.ts index d24d42c..efd393f 100644 --- a/src/component/usePortal.ts +++ b/src/component/usePortal.ts @@ -7,7 +7,7 @@ import { UsePortal, } from "@/definition"; import { usePortalImplementation } from "@/addons"; -import { getValue } from "@/utilities"; +import { getResolvedState, getValue } from "@/utilities"; import { cookieStorage } from "./cookieStorage"; /** @@ -29,7 +29,7 @@ export function usePortal< >(path: Path, options?: PortalOptions) { const initialState = options?.store ? getValue(options.store, path) - : options?.state; + : getResolvedState(options?.state); return usePortalImplementation({ path, @@ -63,7 +63,7 @@ function useLocal< const initialState = config?.store ? getValue(config.store, path) - : config?.state; + : getResolvedState(config?.state); return usePortalImplementation({ path, @@ -97,7 +97,7 @@ function useSession< const initialState = config?.store ? getValue(config.store, path) - : config?.state; + : getResolvedState(config?.state); return usePortalImplementation({ path, @@ -132,7 +132,7 @@ function useCookie< const initialState = config?.store ? getValue(config.store, path) - : config?.state; + : getResolvedState(config?.state); return usePortalImplementation({ path, diff --git a/src/definition/cookie.ts b/src/definition/cookie.ts index 65f9e21..2bfc3e8 100644 --- a/src/definition/cookie.ts +++ b/src/definition/cookie.ts @@ -47,8 +47,7 @@ export type CookieOptions = { }; /** - * Represents a cookie entry with additional properties for storing state data. - * @template S The type of the state value to be stored in the cookie entry. + * Represents a cookie entry in the cookie storage. */ export type CookieEntry = CookieOptions & { /** @@ -103,4 +102,4 @@ export interface CookieStorage extends Storage { * @returns {string | null} The cookie value if found, or null if not found. */ key: (index: number) => string | null; -} \ No newline at end of file +} diff --git a/src/definition/portal.ts b/src/definition/portal.ts index 8c7567e..399531e 100644 --- a/src/definition/portal.ts +++ b/src/definition/portal.ts @@ -16,9 +16,11 @@ export type PortalOptions = { * The initial value of the portal. * * @description - * If the `path` is defined within the portal, the state will be ignored. + * - This value is only used when the `path` is not defined within the portal. + * - This value will be overidden if the `get` method is defined. + * - It uses the `useState` hook internally. */ - state?: State; + state?: State | (() => State); /** * Select the required data from the state. @@ -43,9 +45,8 @@ export type PortalOptions = { * Method to get the initial value. * * @description - * - When the `get` method is undefined, the initial value will be used. - * - If `override` is false, the value returned will not override the initial value. - * - This method is only called once, except when the `key` changes. + * - This method is only called when the `path` is not defined within the portal. + * - It uses the `useEffect` hook internally. */ get?: GetState; }; @@ -98,6 +99,14 @@ export type PortalValue = { observable: BehaviorSubject; }; +/** + * Represents the options for the usePortal hook. + * + * @template State The type of the state. + * @template Path The type of the path. + * @template Store The type of the store. + * @template Data The type of the data. + */ export interface UsePortalImplementation< Store extends Record, Path extends Paths, @@ -111,14 +120,14 @@ export interface UsePortalImplementation< /** * Represents a map of keys and values in the portal entries. - * @template S The type of the store value. - * @template A The type of the action for the reducer. + * @template State The type of the state. + * @template Path The type of the path. */ export type PortalMap = Map>; /** * Represents the result of the usePortal hook. - * @template State The type of the store value. + * @template State The type of the state. */ export type PortalState = [ Data, diff --git a/src/subject/behaviorSubject.ts b/src/subject/behaviorSubject.ts index 6b1536c..0300f32 100644 --- a/src/subject/behaviorSubject.ts +++ b/src/subject/behaviorSubject.ts @@ -3,15 +3,15 @@ import { isSetStateFunction } from "@/utilities"; import type { Subscription } from "@/definition"; -abstract class Subject { - abstract next(value: S): void; - abstract subscribe(observer: (value: S) => void): Subscription; +abstract class Subject { + abstract next(value: State): void; + abstract subscribe(observer: (value: State) => void): Subscription; abstract unsubscribe(): void; } /** * Represents a subject that maintains a current value and emits it to subscribers. - * @template S The type of the initial and emitted values. + * @template State The type of the initial and emitted values. */ export class BehaviorSubject implements Subject { private state: State; @@ -51,7 +51,7 @@ export class BehaviorSubject implements Subject { /** * Returns the current value of the subject. - * @returns {S} The current value. + * @returns {State} The current value. */ get value(): State { return this.state; @@ -59,7 +59,7 @@ export class BehaviorSubject implements Subject { /** * Emits a new value to the subject and notifies subscribers. - * @param {S} value The new value to emit. + * @param {State} value The new value to emit. */ next = (value: State) => { if (!Object.is(this.state, value)) { @@ -69,17 +69,12 @@ export class BehaviorSubject implements Subject { }; /** - * Update the state using the provided value or action. + * Update the state using the provided value. + * @description The updated state is emitted through the `observable.next()` method. + * * @template State The type of the state. * - * @param {SetStateAction} value Value or action to update the state with. - * - * @summary If a dispatch function is provided, it is used to process the state update based on the previous state and the value or action. - * @summary If the dispatch function is not provided and the value is a function, it is called with the previous state and the return value is used as the new state. - * @summary If neither a dispatch function is provided nor the value is a function, the value itself is used as the new state. - * - * @description The updated state is emitted through the observable.next() method. - * + * @param {SetStateAction} value Value to update the state with. * @returns void */ setter = (value: SetStateAction) => { diff --git a/src/subject/portal.ts b/src/subject/portal.ts index 18c39f2..e1b7165 100644 --- a/src/subject/portal.ts +++ b/src/subject/portal.ts @@ -2,8 +2,6 @@ import { handleSSRError } from "@/utilities"; import { cookieStorage } from "@/component"; import type { - CookieEntry, - CookieOptions, PortalMap, PortalValue, SetStore, @@ -35,7 +33,6 @@ class Portal { * * @param {string} path The path of the item to be retrieved. * @param {State} initialState The initial state of the item. - * @param {boolean} [override=false] Whether to override an existing item with the same path. * * @returns {PortalValue} The portal entry with the specified path, or a new portal entry if not found. */ @@ -51,6 +48,7 @@ class Portal { observable: new BehaviorSubject(initialState), storage: new Set>(), }; + this.portalMap.set(path, subject); return subject; }; diff --git a/src/utilities/getComputedState.ts b/src/utilities/getComputedState.ts index 91f648d..9fe966c 100644 --- a/src/utilities/getComputedState.ts +++ b/src/utilities/getComputedState.ts @@ -3,14 +3,14 @@ import { isSetStateFunction } from "./isSetStateFunction"; /** * Gets the actual state value from the provided initial state. - * @template S The type of the initial state value. - * @param {S | ((prevState: S) => S)} initialState The initial state value or a function that returns the initial state. - * @returns {S} The actual state value. + * @template State The type of the initial state value. + * @param {State | ((prevState: State) => State)} initialState The initial state value or a function that returns the initial state. + * @returns {State} The actual state value. */ -export function getComputedState( - initialState: SetStateAction, - previousState: S -) { +export function getComputedState( + initialState: SetStateAction, + previousState: State +): State { if (isSetStateFunction(initialState)) return initialState(previousState); return initialState; } diff --git a/src/utilities/getResolvedState.ts b/src/utilities/getResolvedState.ts new file mode 100644 index 0000000..dba6603 --- /dev/null +++ b/src/utilities/getResolvedState.ts @@ -0,0 +1,14 @@ +import { isFunction } from "./isFunction"; + +/** + * Gets the actual state value from the provided initial state. + * @template State The type of the initial state value. + * @param {State | (() => State)} initialState The initial state value or a function that returns the initial state. + * @returns {State} The actual state value. + */ +export function getResolvedState( + initialState: State | (() => State) +): State { + if (isFunction(initialState)) return initialState(); + return initialState; +} diff --git a/src/utilities/index.ts b/src/utilities/index.ts index ccaf369..eb1402d 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,4 +1,5 @@ export * from "./getComputedState"; +export * from "./getResolvedState"; export * from "./handleSSRError"; export * from "./getValue"; export * from "./isAtomStateFunction"; diff --git a/src/utilities/isSetStateFunction.ts b/src/utilities/isSetStateFunction.ts index 447f7f1..01fa978 100644 --- a/src/utilities/isSetStateFunction.ts +++ b/src/utilities/isSetStateFunction.ts @@ -3,8 +3,8 @@ import { SetStateAction } from "react"; /** * Type guard to check if a value is a SetStateAction function. * - * @template S The type of the state. - * @param {SetStateAction} value The value to be checked. + * @template State The type of the state. + * @param {SetStateAction} value The value to be checked. * @returns {boolean} `true` if the value is a SetStateAction function, otherwise `false`. */ export function isSetStateFunction( diff --git a/src/utilities/objectToStringKey.ts b/src/utilities/objectToStringKey.ts index 1cee2c9..de057af 100644 --- a/src/utilities/objectToStringKey.ts +++ b/src/utilities/objectToStringKey.ts @@ -2,7 +2,7 @@ type Primitives = string | number | bigint | boolean | null | undefined; type ToString = T extends Primitives ? `${T}` : Extract; type ArrayToString = T extends [infer First, ...infer Rest] - ? `${ToString}${Rest extends [] ? "" : ","}${ObjectToStringKey}` + ? `${ToString}${Rest extends [] ? "" : ","}${ArrayToString}` : ""; type UnionToIntersection = ( @@ -25,8 +25,9 @@ type Construct = ObjectToStringKey extends infer U type Flat = UnionToTuple extends [infer First, ...infer Rest] ? First extends keyof T - ? `${Construct}${Rest extends [] ? "" : ";"}${ObjectToString< - Pick> + ? `${Construct}${Rest extends [] ? "" : ";"}${Flat< + Exclude, + T >}` : never : never; @@ -37,30 +38,21 @@ type ObjectToString = keyof T extends never ? Flat : never; -type ObjectToStringKey = T extends any[] +type ObjectToStringKey = T extends string + ? T + : T extends any[] ? ArrayToString : T extends object ? ObjectToString : ToString; -type NestedObject< - T extends Record, - P extends string[] -> = P extends [infer First, ...infer Rest] - ? First extends string - ? Rest extends string[] - ? { [K in First]: NestedObject } - : never - : never - : T; - /** * Converts a reference type to a string representation that can be used as a key. * * @param {any} value The value to convert. * @returns {string} The string representation of the value. */ -export function objectToStringKey(value: any): string { +export function objectToStringKey(value: T): string { if (typeof value === "object" && value !== null) { if (Array.isArray(value)) { const arrayString = value.map(objectToStringKey).join(",");