react-hooks-global-states
Version:
This is a package to easily handling global-state across your react-components using hooks.
561 lines (560 loc) • 25 kB
TypeScript
import React, { type PropsWithChildren, Context as ReactContext } from 'react';
import { GlobalStore } from './GlobalStore';
import type { BaseMetadata, GlobalStoreCallbacks as GlobalStoreCallbacksBase, UseHookOptions, AnyFunction, StoreTools, ReadonlyStateApi, SelectHook, ObservableFragment, StateApi, UnsubscribeCallback } from './types';
import { AnyActions } from 'createGlobalState';
type GlobalStoreContextCallbacks<State, StateMutator, Metadata extends BaseMetadata> = {
/**
* @description Optional callback invoked after the context is created,
*/
onCreated?: (
/**
* @description Full context instance
*/
storeTools: ContextStoreTools<State, StateMutator extends AnyFunction ? null : StateMutator, Metadata>,
/**
* @description Underlying store instance
*/
store: GlobalStore<State, Metadata, unknown, any>) => void;
/**
* @description Called when the context provider is mounted
*/
onMounted?: (
/**
* @description Full context instance
*/
storeTools: ContextStoreTools<State, StateMutator extends AnyFunction ? null : StateMutator, Metadata>,
/**
* @description Underlying store instance
*/
store: GlobalStore<State, Metadata, unknown, any>) => void | UnsubscribeCallback;
};
export type GlobalStoreCallbacks<State, StateMutator, Metadata extends BaseMetadata> = GlobalStoreCallbacksBase<State, StateMutator, Metadata> & GlobalStoreContextCallbacks<State, StateMutator, Metadata>;
/**
* @description Resulting type of the action collection configuration
*/
export type ContextActionCollectionResult<State, Metadata extends BaseMetadata, ActionsConfig extends ContextActionCollectionConfig<State, Metadata>> = {
[key in keyof ActionsConfig]: {
(...params: Parameters<ActionsConfig[key]>): ReturnType<ReturnType<ActionsConfig[key]>>;
};
};
/**
* contract for the storeActionsConfig configuration
*/
export interface ContextActionCollectionConfig<State, Metadata extends BaseMetadata, ThisAPI = Record<string, (...parameters: any[]) => unknown>> {
readonly [key: string]: {
(this: ThisAPI, ...parameters: any[]): (this: ThisAPI, storeTools: ContextStoreTools<State, Record<string, (...parameters: any[]) => unknown | void>, Metadata>) => unknown | void;
};
}
/**
* @description Extensions methods for the context store tools
*/
export type ContextStoreToolsExtensions<State, StateMutator, Metadata extends BaseMetadata> = {
/**
* @description Main hook of the context
*
* @example
* ```tsx
* type CounterContext = import('../../stores/counter').CounterContext;
*
* const useLogCount = () => {
* return ({ use }: CounterContext) => {
* const count = use.select(s => s.count);
*
* console.log('Count changed:', count);
* };
* }
*
* // Usage in store
* import useLogCount from './actions/useLogCount';
*
* const counter = createContext({ count: 0 }, {
* actions: {
* useLogCount,
* }
* });
*
* // Usage in component
* import counter from '../stores/counter';
*
* const CounterLogger = () => {
* // access to the actions is NOT-REACTIVE
* const { useLogCount } = counter.use.actions();
*
* useLogCount();
* }
* ```
*/
use: ContextHook<State, StateMutator, Metadata>;
};
/**
* @description Store tools specialized for context usage
*/
export type ContextStoreTools<State, StateMutator, Metadata extends BaseMetadata> = StoreTools<State, StateMutator, Metadata> & ContextStoreToolsExtensions<State, StateMutator, Metadata>;
/**
* @description Extensions methods for the ContextProvider component
*/
export type ContextProviderExtensions<State, StateMutator, Metadata extends BaseMetadata> = {
/**
* Creates a provider wrapper which allows to capture the context value,
* useful for testing purposes.
* @param options configuration options for the provider wrapper
* @param options.value optional initial state or initializer function
* @param options.onCreated optional callback invoked after the context is created
* @returns an object containing the wrapper component and a reference to the context value
*/
makeProviderWrapper: (options?: {
/**
* @description Optional initial state or initializer function, useful for testing, storybooks, etc.
*/
value?: State | ((initialValue: State) => State);
} & GlobalStoreContextCallbacks<State, StateMutator extends AnyFunction ? null : StateMutator, Metadata>) => {
/**
* Provider for the context
*/
wrapper: React.FC<PropsWithChildren<{
/**
* @description Optional initial state or initializer function, useful for testing, storybooks, etc.
*/
value?: State | ((initialValue: State) => State);
}>>;
/**
* Reference to the current context value
*/
context: {
/**
* @description Current context value
*/
current: ContextStoreTools<State, StateMutator extends AnyFunction ? null : StateMutator, Metadata>;
/**
* @description Underlying store instance
*/
instance: GlobalStore<State, Metadata, unknown, any>;
};
};
};
/**
* @description Creates a React context provider component for the given global state.
* @param value Optional initial state or initializer function, useful for testing.
* @param onCreated Optional callback invoked after the context is created, receiving the full context instance.
*/
export type ContextProvider<State, StateMutator, Metadata extends BaseMetadata> = React.FC<PropsWithChildren<{
/**
* @description Optional initial state or initializer function, useful for testing, storybooks, etc.
*/
value?: State | ((initialValue: State) => State);
} & GlobalStoreContextCallbacks<State, StateMutator extends AnyFunction ? null : StateMutator, Metadata>>> & ContextProviderExtensions<State, StateMutator, Metadata>;
/**
* @description Represents a hook that returns a readonly state.
*/
interface ReadonlyContextHook<State, StateMutator, Metadata extends BaseMetadata> extends ReadonlyContextPublicApi<State, StateMutator, Metadata> {
/**
* @description Returns the full state.
*/
(): State;
/**
* @description Returns a derived value from the state using the provided selector function.
* @param selector A function that selects a part of the state.
* @param dependencies Optional array of dependencies to control when the selector is re-evaluated.
* @returns The derived value from the state.
*/
<Derivate>(selector: (state: State) => Derivate, dependencies?: unknown[]): Derivate;
/**
* @description Returns a derived value from the state using the provided selector function.
* @param selector A function that selects a part of the state.
* @param options Optional configuration for the selector hook.
* @param options.isEqual Optional equality function to compare the selected fragment.
* @param options.isEqualRoot Optional equality function to compare the root state.
* @param options.dependencies Optional array of dependencies to control when the selector is re-evaluated.
* @returns The derived value from the state.
*/
<Derivate>(selector: (state: State) => Derivate, options?: UseHookOptions<Derivate, State>): Derivate;
}
/**
* @description Hook for accessing a context's state, mutator (setState or actions), and metadata.
* @returns A read-only tuple containing:
* - state: the current state, or the derived value when a selector is used
* - stateMutator: a function or actions collection to update the state
* - metadata: the current context metadata
*
* @example
* ```tsx
* // Simple usage (full state)
* const [state, setState] = useTodosContext();
*
* // With a selector (preferred for render isolation)
* const [todos, actions] = useTodosContext(s => s.todos);
*
* actions.setTodos(next);
* ```
*/
export interface ContextHook<State, StateMutator, Metadata extends BaseMetadata> extends ContextPublicApi<State, StateMutator, Metadata> {
/**
* @description Retrieves the full state, state mutator (setState or actions), and metadata.
*/
(): Readonly<[state: State, stateMutator: StateMutator, metadata: Metadata]>;
/**
* @description Retrieves a derived value from the state using the provided selector function.
* @param selector A function that selects a part of the state.
* @param dependencies Optional array of dependencies to control when the selector is re-evaluated.
* @returns A read-only tuple containing the derived state, state mutator (setState or actions), and metadata.
*/
<Derivate>(selector: (state: State) => Derivate, dependencies?: unknown[]): Readonly<[state: Derivate, stateMutator: StateMutator, metadata: Metadata]>;
/**
* @description Retrieves a derived value from the state using the provided selector function.
* @param selector A function that selects a part of the state.
* @param dependencies Optional array of dependencies to control when the selector is re-evaluated.
* @returns A read-only tuple containing the derived state, state mutator (setState or actions), and metadata.
*/
<Derivate>(selector: (state: State) => Derivate, config?: UseHookOptions<Derivate, State>): Readonly<[state: Derivate, stateMutator: StateMutator, metadata: Metadata]>;
}
export type ContextPublicApi<State, StateMutator, Metadata extends BaseMetadata> = {
/**
* @description [NOT A HOOK, NON-REACTIVE]
* Creates a derived hook that subscribes to a selected fragment of the context state.
* The selector determines which portion of the state the new hook exposes.
* This hook must be used within the corresponding context provider.
*
* @param selector A function that selects a part of the state.
* @param args Optional configuration for the derived hook, including:
* - isEqual: A function to compare the current and next selected fragment for equality.
* - isEqualRoot: A function to compare the entire state for equality.
* - name: An optional name for debugging purposes.
* @returns A new context hook that provides access to the selected fragment of the state,
* along with the state mutator and metadata.
*
* @example
* ```tsx
* const useTodos = createContext({
* todos: [],
* filter: '',
* }, {
* actions: {
* setFilter(filter: string) {
* ...
* });
*
* const useFilter = useTodos.createSelectorHook((state) => {
* return state.filter;
* });
*
* function FilterComponent() {
* // The selector only listen to the selected fragment (filter)
* // But has access to the full actions collection
* const [filter, { setFilter }] = useFilter();
*
* return (
* <input
* value={filter}
* onChange={(e) => setFilter(e.target.value)}
* />
* );
* }
* ```
*/
createSelectorHook: <Derivate>(this: ReadonlyContextPublicApi<State, StateMutator, Metadata>, selector: (state: State) => Derivate, args?: Omit<UseHookOptions<Derivate, State>, 'dependencies'> & {
name?: string;
}) => ReadonlyContextHook<Derivate, StateMutator, Metadata>;
/**
* @description [NON-REACTIVE]
* Hook that provides non-reactive access to the context API.
* This allows direct interaction with the context’s state, metadata, and actions
* without triggering component re-renders.
* @returns An object containing the context API methods and properties.
*/
api: () => StateApi<State, StateMutator, Metadata>;
/**
* @description [NON-REACTIVE]
* Provides direct access to the context's actions, if available.
*/
actions: () => StateMutator extends AnyFunction ? null : StateMutator;
/**
* @description Selects a fragment of the state using the provided selector function.
*/
select: SelectHook<State>;
/**
* @description
* Creates a hook that allows you to subscribe to a fragment of the state
* The observable selection will notify the subscribers only if the fragment changes and the equality function returns false
* you can customize the equality function by passing the isEqualRoot and isEqual parameters
*
* @example
* ```tsx
* const observable = store.use.observable(state => state.count);
*
* useEffect(() => {
* const unsubscribe = observable.subscribe(() => {
* // do something when the selected fragment changes
* });
*
* return () => {
* unsubscribe();
* };
* }, [observable]);
* ```
*/
observable: <Selection>(selector: (state: State) => Selection, args?: {
isEqual?: (current: Selection, next: Selection) => boolean;
isEqualRoot?: (current: State, next: State) => boolean;
/**
* @description Name of the observable fragment for debugging purposes
*/
name?: string;
}) => ObservableFragment<Selection, StateMutator, Metadata>;
/**
* @description display name for debugging purposes
*/
readonly displayName: string;
};
/**
* @description Readonly version of the ContextPublicApi, expose by selectors and observables
*/
export type ReadonlyContextPublicApi<State, StateMutator, Metadata extends BaseMetadata> = Pick<ContextPublicApi<State, StateMutator, Metadata>, 'createSelectorHook' | 'displayName'> & {
/**
* @description Hook that provides non-reactive access to the context API.
* This allows direct interaction with the context’s state, metadata, and actions
* without triggering component re-renders.
* @returns An object containing the context API methods and properties.
*/
api: () => ReadonlyStateApi<State, StateMutator, Metadata>;
};
interface CreateContext {
/**
* @description Creates a highly granular React context with its associated provider and state hook.
* @param value Initial state value or initializer function.
* @returns An object containing:
* - **`use`** — A custom hook to read and mutate the context state.
* Supports selectors for granular subscriptions and returns `[state, stateMutator, metadata]`.
* - **`Provider`** — A React component that provides the context to its descendants.
* It accepts an optional initial value and `onCreated` callback.
* - **`Context`** — The raw React `Context` object for advanced usage, such as integration with
* external tools or non-React consumers.
*/
<State, StateMutator = React.Dispatch<React.SetStateAction<State>>>(value: State | (() => State)): {
/**
* @description Hook and API for interacting with the context.
* This hook provides access to the context state, actions, and metadata.
*
* There are two ways to use the hook
* @example
* The more simple and familiar way is to use it as a regular hook
*
* ```tsx
* const { Context, Provider, user: useUser} = createContext({ name: 'John', age: 30 });
*
* function UserProfile() {
* const [state, setState, metadata] = useUser();
*
* ....
* }
* ```
*
* @example
* The recommended, more sematic and easier to read way:
*
* ```tsx
* const user = createContext({ name: 'John', age: 30 });
*
* <user.Provider>
* <UserProfile />
* </user.Provider>
*
* function UserProfile() {
* const [state, setState, metadata] = user.use();
* const userContext = user.use.api();
* const userName = user.use.select(s => s.name);
* // ...
* }
*/
use: ContextHook<State, StateMutator, BaseMetadata>;
/**
* @description Provider for the context
*/
Provider: ContextProvider<State, StateMutator, BaseMetadata>;
/**
* @description The raw React Context object
*/
Context: ReactContext<ContextHook<State, StateMutator, BaseMetadata> | null>;
};
/**
* @description Creates a highly granular React context with its associated provider and state hook.
* @param value Initial state value or initializer function.
* @param args Additional configuration for the context.
* @param args.name Optional name for debugging purposes.
* @param args.metadata Optional non-reactive metadata associated with the state.
* @param args.callbacks Optional lifecycle callbacks for the context.
* @param args.actions Optional actions to restrict state mutations [if provided `setState` will be nullified].
* @returns An object containing:
* - **`use`** — A custom hook to read and mutate the context state.
* Supports selectors for granular subscriptions and returns `[state, stateMutator, metadata]`.
* - **`Provider`** — A React component that provides the context to its descendants.
* It accepts an optional initial value and `onCreated` callback.
* - **`Context`** — The raw React `Context` object for advanced usage, such as integration with
* external tools or non-React consumers.
*/
<State, Metadata extends BaseMetadata, ActionsConfig extends ContextActionCollectionConfig<State, Metadata> | null | {}, StateMutator = keyof ActionsConfig extends never | undefined ? React.Dispatch<React.SetStateAction<State>> : ContextActionCollectionResult<State, Metadata, NonNullable<ActionsConfig>>>(
/**
* @description Initial state value or initializer function.
*/
value: State | (() => State),
/**
* @description Additional configuration for the context.
* @param args.name Optional name for debugging purposes.
* @param args.metadata Optional non-reactive metadata associated with the state.
* @param args.callbacks Optional lifecycle callbacks for the context.
* @param args.actions Optional actions to restrict state mutations [if provided `setState` will be nullified].
*/
args: {
name?: string;
metadata?: Metadata | (() => Metadata);
callbacks?: GlobalStoreCallbacks<State, AnyActions, Metadata>;
actions?: ActionsConfig;
}): {
/**
* @description Hook and API for interacting with the context.
* This hook provides access to the context state, actions, and metadata.
*
* There are two ways to use the hook
* @example
* The more simple and familiar way is to use it as a regular hook
*
* ```tsx
* const { Context, Provider, user: useUser} = createContext({ name: 'John', age: 30 });
*
* function UserProfile() {
* const [state, setState, metadata] = useUser();
*
* ....
* }
* ```
*
* @example
* The recommended, more sematic and easier to read way:
*
* ```tsx
* const user = createContext({ name: 'John', age: 30 });
*
* <user.Provider>
* <UserProfile />
* </user.Provider>
*
* function UserProfile() {
* const [state, setState, metadata] = user.use();
* const userContext = user.use.api();
* const userName = user.use.select(s => s.name);
* // ...
* }
*/
use: ContextHook<State, StateMutator, Metadata>;
/**
* @description Provider for the context
*/
Provider: ContextProvider<State, StateMutator, Metadata>;
/**
* @description Raw React Context object
*/
Context: ReactContext<ContextHook<State, StateMutator, Metadata> | null>;
};
/**
* @description Creates a highly granular React context with its associated provider and state hook.
* @param value Initial state value or initializer function.
* @param args Additional configuration for the context.
* @param args.name Optional name for debugging purposes.
* @param args.metadata Optional non-reactive metadata associated with the state.
* @param args.callbacks Optional lifecycle callbacks for the context.
* @param args.actions Optional actions to restrict state mutations [if provided `setState` will be nullified].
* @returns An object containing:
* - **`use`** — A custom hook to read and mutate the context state.
* Supports selectors for granular subscriptions and returns `[state, stateMutator, metadata]`.
* - **`Provider`** — A React component that provides the context to its descendants.
* It accepts an optional initial value and `onCreated` callback.
* - **`Context`** — The raw React `Context` object for advanced usage, such as integration with
* external tools or non-React consumers.
*/
<State, Metadata extends BaseMetadata, ActionsConfig extends ContextActionCollectionConfig<State, Metadata>>(
/**
* @description Initial state value or initializer function.
*/
value: State | (() => State),
/**
* @description Additional configuration for the context.
* @param args.name Optional name for debugging purposes.
* @param args.metadata Optional non-reactive metadata associated with the state.
* @param args.callbacks Optional lifecycle callbacks for the context.
* @param args.actions Optional actions to restrict state mutations [if provided `setState` will be nullified].
*/
args: {
name?: string;
metadata?: Metadata | (() => Metadata);
callbacks?: GlobalStoreCallbacks<State, AnyActions, Metadata>;
actions: ActionsConfig;
}): {
/**
* @description Hook and API for interacting with the context.
* This hook provides access to the context state, actions, and metadata.
*
* There are two ways to use the hook
* @example
* The more simple and familiar way is to use it as a regular hook
*
* ```tsx
* const { Context, Provider, user: useUser} = createContext({ name: 'John', age: 30 });
*
* function UserProfile() {
* const [state, setState, metadata] = useUser();
*
* ....
* }
* ```
*
* @example
* The recommended, more sematic and easier to read way:
*
* ```tsx
* const user = createContext({ name: 'John', age: 30 });
*
* <user.Provider>
* <UserProfile />
* </user.Provider>
*
* function UserProfile() {
* const [state, setState, metadata] = user.use();
* const userContext = user.use.api();
* const userName = user.use.select(s => s.name);
* // ...
* }
*/
use: ContextHook<State, ContextActionCollectionResult<State, Metadata, ActionsConfig>, Metadata>;
/**
* @description Provider for the context
*/
Provider: ContextProvider<State, ContextActionCollectionResult<State, Metadata, ActionsConfig>, Metadata>;
/**
* @description Raw React Context object
*/
Context: ReactContext<ContextHook<State, ContextActionCollectionResult<State, Metadata, ActionsConfig>, Metadata> | null>;
};
}
/**
* @description Creates a highly granular React context with its associated provider and state hook.
* Unlike the native `React.createContext`, this version provides fine-grained reactivity and supports
* state selection, metadata handling, and optional custom actions for controlled mutations.
*
* Components using the generated hook only re-render when the selected part of the state changes,
* making it efficient for large or deeply nested state trees.
*/
export declare const createContext: CreateContext;
/**
* @description Infers the context API type
*
* @example
* ```ts
* const counter = createContext(0);
*
* type CounterContextApi = InferContextApi<typeof counter.Context>;
*
* // Equivalent to:
* ContextApi<number, React.Dispatch<React.SetStateAction<number>>, BaseMetadata>;
* ```
*/
export type InferContextApi<Context extends ReactContext<ContextHook<any, any, any> | null>> = NonNullable<React.ContextType<Context>> extends ContextHook<infer State, infer StateMutator, infer Metadata> ? ContextStoreTools<State, StateMutator extends AnyFunction ? null : StateMutator, Metadata> : never;
export default createContext;