Files
v10/packages/store/src/react/create-store.tsx
T

186 lines
5.4 KiB
TypeScript

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<Slices extends AnySlice[]> extends StoreConfig<UnionSliceTarget<Slices>, Slices> {
/**
* Display name for React DevTools.
*/
displayName?: string;
}
export interface ProviderProps<Slices extends AnySlice[]> {
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<UnionSliceTarget<Slices>, 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<Slices extends AnySlice[]> {
/**
* Provider component that creates and manages the store lifecycle.
*/
Provider: FC<ProviderProps<Slices>>;
/**
* Returns the typed store instance from context.
*/
useStore: () => Store<UnionSliceTarget<Slices>, Slices>;
/**
* Subscribes to a selected portion of state.
* Re-renders only when the selected value changes.
*/
useSelector: <T>(selector: (state: UnionSliceState<Slices>) => T) => T;
/**
* Returns the request map or a selected request.
*/
useRequest: {
(): UnionSliceRequests<Slices>;
<T>(selector: (requests: UnionSliceRequests<Slices>) => T): T;
};
/**
* Subscribes to task state changes.
* Returns the current tasks map from the queue.
*/
useTasks: () => TasksRecord<UnionSliceTasks<Slices>>;
/**
* Creates a new store instance.
* Useful for imperative access or creating a store before render.
*/
create: () => Store<UnionSliceTarget<Slices>, 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<Slices extends AnySlice[]>(config: CreateStoreConfig<Slices>): CreateStoreResult<Slices> {
type Target = UnionSliceTarget<Slices>;
type State = UnionSliceState<Slices>;
type Requests = UnionSliceRequests<Slices>;
type Tasks = UnionSliceTasks<Slices>;
type StoreType = Store<Target, Slices>;
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<Slices>): ReactNode {
const parentStore = useParentStore();
const shouldInherit = inherit && !isNull(parentStore);
const [store] = useState<StoreType>(() => {
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 <StoreContextProvider store={store}>{children}</StoreContextProvider>;
}
// Set display name for React DevTools
if (config.displayName) {
Provider.displayName = `${config.displayName}.Provider`;
}
function useStore(): StoreType {
return useStoreContext() as StoreType;
}
function useSelector<T>(selector: (state: State) => T): T {
const store = useStore();
return useSelectorBase(store, selector);
}
function useRequest(): Requests;
function useRequest<T>(selector: (requests: Requests) => T): T;
function useRequest<T>(selector?: (requests: Requests) => T): Requests | T {
const store = useStore();
const requests = useRequestBase(store);
if (isUndefined(selector)) {
return requests;
}
return selector(requests);
}
function useTasks(): TasksRecord<Tasks> {
const store = useStore();
return useTasksBase(store);
}
return {
Provider,
useStore,
useSelector,
useRequest: useRequest as CreateStoreResult<Slices>['useRequest'],
useTasks,
create,
};
}