import type { FC, ReactNode } from 'react'; import type { TasksRecord } from '../core/queue'; import type { AnySlice, UnionSliceRequests, UnionSliceState, UnionSliceTarget, UnionSliceTasks } from '../core/slice'; import type { StoreConfig } from '../core/store'; import { isNull, isUndefined } from '@videojs/utils/predicate'; import { useEffect, useState } from 'react'; import { Store } from '../core/store'; import { StoreContextProvider, useParentStore, useStoreContext } from './context'; import { useRequest as useRequestBase, useSelector as useSelectorBase, useTasks as useTasksBase } from './hooks'; // ---------------------------------------- // Types // ---------------------------------------- export interface CreateStoreConfig extends StoreConfig, Slices> { /** * Display name for React DevTools. */ displayName?: string; } export interface ProviderProps { children: ReactNode; /** * Optional pre-created store instance. * If provided, the Provider will use this store instead of creating one. * The Provider will NOT destroy this store on unmount. */ store?: Store, Slices>; /** * If true, inherits the store from a parent Provider context instead of creating a new one. * Useful when wrapping a skin with your own Provider to add custom hooks. * Defaults to false (isolated store). */ inherit?: boolean; } export interface CreateStoreResult { /** * Provider component that creates and manages the store lifecycle. */ Provider: FC>; /** * Returns the typed store instance from context. */ useStore: () => Store, Slices>; /** * Subscribes to a selected portion of state. * Re-renders only when the selected value changes. */ useSelector: (selector: (state: UnionSliceState) => T) => T; /** * Returns the request map or a selected request. */ useRequest: { (): UnionSliceRequests; (selector: (requests: UnionSliceRequests) => T): T; }; /** * Subscribes to task state changes. * Returns the current tasks map from the queue. */ useTasks: () => TasksRecord>; /** * Creates a new store instance. * Useful for imperative access or creating a store before render. */ create: () => Store, Slices>; } // ---------------------------------------- // Implementation // ---------------------------------------- /** * Creates a store factory that returns a Provider and typed hooks. * * @param config - Store configuration including slices and optional lifecycle hooks * @returns An object containing Provider, hooks, and a create function * * @example * ```tsx * const { Provider, useStore, useSelector, useRequest, useTasks, create } = createStore({ * slices: [playbackSlice, presentationSlice], * }); * ``` */ export function createStore(config: CreateStoreConfig): CreateStoreResult { type Target = UnionSliceTarget; type State = UnionSliceState; type Requests = UnionSliceRequests; type Tasks = UnionSliceTasks; type StoreType = Store; function create(): StoreType { return new Store(config); } /** * Provider component that manages store lifecycle. * * Resolution order: * 1. If `store` prop provided, uses that store (no cleanup on unmount) * 2. If `inherit={true}` and parent store exists, uses parent store (no cleanup) * 3. Otherwise, creates a new store and destroys it on unmount */ function Provider({ children, store: providedStore, inherit = false }: ProviderProps): ReactNode { const parentStore = useParentStore(); const shouldInherit = inherit && !isNull(parentStore); const [store] = useState(() => { if (!isUndefined(providedStore)) { return providedStore; } if (shouldInherit) { return parentStore as StoreType; } return create(); }); // Only destroy if we created the store (not provided, not inherited) const isOwner = isUndefined(providedStore) && !shouldInherit; useEffect(() => { if (isOwner) { return () => store.destroy(); } return undefined; }, [store, isOwner]); return {children}; } // Set display name for React DevTools if (config.displayName) { Provider.displayName = `${config.displayName}.Provider`; } function useStore(): StoreType { return useStoreContext() as StoreType; } function useSelector(selector: (state: State) => T): T { const store = useStore(); return useSelectorBase(store, selector); } function useRequest(): Requests; function useRequest(selector: (requests: Requests) => T): T; function useRequest(selector?: (requests: Requests) => T): Requests | T { const store = useStore(); const requests = useRequestBase(store); if (isUndefined(selector)) { return requests; } return selector(requests); } function useTasks(): TasksRecord { const store = useStore(); return useTasksBase(store); } return { Provider, useStore, useSelector, useRequest: useRequest as CreateStoreResult['useRequest'], useTasks, create, }; }