Skip to content

Commit

Permalink
Make usePortal initial state either a state or function of the state.
Browse files Browse the repository at this point in the history
  • Loading branch information
ibnlanre committed Nov 22, 2023
1 parent 54e6f39 commit 80868ee
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 60 deletions.
2 changes: 1 addition & 1 deletion src/addons/withImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function usePortalImplementation<

/**
* Return an array containing the current state and the setter function for state updates.
* @type {PortalState<S, A>}
* @type {PortalState<State>}
*/
return [select(observable.value), observable.setter];
}
10 changes: 5 additions & 5 deletions src/component/usePortal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -29,7 +29,7 @@ export function usePortal<
>(path: Path, options?: PortalOptions<Store, State, Data>) {
const initialState = options?.store
? getValue(options.store, path)
: options?.state;
: getResolvedState(options?.state);

return usePortalImplementation<Store, Path, State, Data>({
path,
Expand Down Expand Up @@ -63,7 +63,7 @@ function useLocal<

const initialState = config?.store
? getValue(config.store, path)
: config?.state;
: getResolvedState(config?.state);

return usePortalImplementation<Store, Path, State, Data>({
path,
Expand Down Expand Up @@ -97,7 +97,7 @@ function useSession<

const initialState = config?.store
? getValue(config.store, path)
: config?.state;
: getResolvedState(config?.state);

return usePortalImplementation<Store, Path, State, Data>({
path,
Expand Down Expand Up @@ -132,7 +132,7 @@ function useCookie<

const initialState = config?.store
? getValue(config.store, path)
: config?.state;
: getResolvedState(config?.state);

return usePortalImplementation<Store, Path, State, Data>({
path,
Expand Down
5 changes: 2 additions & 3 deletions src/definition/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
/**
Expand Down Expand Up @@ -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;
}
}
25 changes: 17 additions & 8 deletions src/definition/portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ export type PortalOptions<Store, State, Data = State> = {
* 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.
Expand All @@ -43,9 +45,8 @@ export type PortalOptions<Store, State, Data = State> = {
* 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<State>;
};
Expand Down Expand Up @@ -98,6 +99,14 @@ export type PortalValue<State> = {
observable: BehaviorSubject<State>;
};

/**
* 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<string, any>,
Path extends Paths<Store>,
Expand All @@ -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<State, Path> = Map<Path, PortalValue<State>>;

/**
* 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<State, Data = State> = [
Data,
Expand Down
25 changes: 10 additions & 15 deletions src/subject/behaviorSubject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { isSetStateFunction } from "@/utilities";

import type { Subscription } from "@/definition";

abstract class Subject<S> {
abstract next(value: S): void;
abstract subscribe(observer: (value: S) => void): Subscription;
abstract class Subject<State> {
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<State> implements Subject<State> {
private state: State;
Expand Down Expand Up @@ -51,15 +51,15 @@ export class BehaviorSubject<State> implements Subject<State> {

/**
* Returns the current value of the subject.
* @returns {S} The current value.
* @returns {State} The current value.
*/
get value(): State {
return this.state;
}

/**
* 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)) {
Expand All @@ -69,17 +69,12 @@ export class BehaviorSubject<State> implements Subject<State> {
};

/**
* 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<S, A>} 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<State>} value Value to update the state with.
* @returns void
*/
setter = (value: SetStateAction<State>) => {
Expand Down
4 changes: 1 addition & 3 deletions src/subject/portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { handleSSRError } from "@/utilities";
import { cookieStorage } from "@/component";

import type {
CookieEntry,
CookieOptions,
PortalMap,
PortalValue,
SetStore,
Expand Down Expand Up @@ -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<State, Path>} The portal entry with the specified path, or a new portal entry if not found.
*/
Expand All @@ -51,6 +48,7 @@ class Portal {
observable: new BehaviorSubject(initialState),
storage: new Set<SetStore<State>>(),
};

this.portalMap.set(path, subject);
return subject;
};
Expand Down
14 changes: 7 additions & 7 deletions src/utilities/getComputedState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<S>(
initialState: SetStateAction<S>,
previousState: S
) {
export function getComputedState<State>(
initialState: SetStateAction<State>,
previousState: State
): State {
if (isSetStateFunction(initialState)) return initialState(previousState);
return initialState;
}
14 changes: 14 additions & 0 deletions src/utilities/getResolvedState.ts
Original file line number Diff line number Diff line change
@@ -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<State>(
initialState: State | (() => State)
): State {
if (isFunction(initialState)) return initialState();
return initialState;
}
1 change: 1 addition & 0 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./getComputedState";
export * from "./getResolvedState";
export * from "./handleSSRError";
export * from "./getValue";
export * from "./isAtomStateFunction";
Expand Down
4 changes: 2 additions & 2 deletions src/utilities/isSetStateFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<S>} value The value to be checked.
* @template State The type of the state.
* @param {SetStateAction<State>} value The value to be checked.
* @returns {boolean} `true` if the value is a SetStateAction function, otherwise `false`.
*/
export function isSetStateFunction<State>(
Expand Down
24 changes: 8 additions & 16 deletions src/utilities/objectToStringKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ type Primitives = string | number | bigint | boolean | null | undefined;
type ToString<T> = T extends Primitives ? `${T}` : Extract<T, string>;

type ArrayToString<T extends any[]> = T extends [infer First, ...infer Rest]
? `${ToString<First>}${Rest extends [] ? "" : ","}${ObjectToStringKey<Rest>}`
? `${ToString<First>}${Rest extends [] ? "" : ","}${ArrayToString<Rest>}`
: "";

type UnionToIntersection<U> = (
Expand All @@ -25,8 +25,9 @@ type Construct<K extends keyof T, T> = ObjectToStringKey<T[K]> extends infer U

type Flat<K, T> = UnionToTuple<K> extends [infer First, ...infer Rest]
? First extends keyof T
? `${Construct<First, T>}${Rest extends [] ? "" : ";"}${ObjectToString<
Pick<T, Exclude<keyof T, First>>
? `${Construct<First, T>}${Rest extends [] ? "" : ";"}${Flat<
Exclude<keyof T, First>,
T
>}`
: never
: never;
Expand All @@ -37,30 +38,21 @@ type ObjectToString<T> = keyof T extends never
? Flat<K, T>
: never;

type ObjectToStringKey<T> = T extends any[]
type ObjectToStringKey<T> = T extends string
? T
: T extends any[]
? ArrayToString<T>
: T extends object
? ObjectToString<T>
: ToString<T>;

type NestedObject<
T extends Record<string, any>,
P extends string[]
> = P extends [infer First, ...infer Rest]
? First extends string
? Rest extends string[]
? { [K in First]: NestedObject<T, Rest> }
: 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<T>(value: T): string {
if (typeof value === "object" && value !== null) {
if (Array.isArray(value)) {
const arrayString = value.map(objectToStringKey).join(",");
Expand Down

0 comments on commit 80868ee

Please sign in to comment.