refactor(store): rename slice to feature (#318)

This commit is contained in:
rahim
2026-01-21 00:33:28 +11:00
committed by GitHub
parent 5bbec30df9
commit f01a5fcf5c
71 changed files with 777 additions and 978 deletions
+22 -22
View File
@@ -1,6 +1,6 @@
import type { FC, ReactNode } from 'react';
import type { AnyFeature, UnionFeatureRequests, UnionFeatureState, UnionFeatureTarget, UnionFeatureTasks } from '../core/feature';
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';
@@ -15,21 +15,21 @@ import { useRequest as useRequestBase, useSnapshot as useSnapshotBase, useTasks
// Types
// ----------------------------------------
export interface CreateStoreConfig<Slices extends AnySlice[]> extends StoreConfig<UnionSliceTarget<Slices>, Slices> {
export interface CreateStoreConfig<Features extends AnyFeature[]> extends StoreConfig<UnionFeatureTarget<Features>, Features> {
/**
* Display name for React DevTools.
*/
displayName?: string;
}
export interface ProviderProps<Slices extends AnySlice[]> {
export interface ProviderProps<Features extends AnyFeature[]> {
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>;
store?: Store<UnionFeatureTarget<Features>, Features>;
/**
* 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.
@@ -38,42 +38,42 @@ export interface ProviderProps<Slices extends AnySlice[]> {
inherit?: boolean;
}
export interface CreateStoreResult<Slices extends AnySlice[]> {
export interface CreateStoreResult<Features extends AnyFeature[]> {
/**
* Provider component that creates and manages the store lifecycle.
*/
Provider: FC<ProviderProps<Slices>>;
Provider: FC<ProviderProps<Features>>;
/**
* Returns the typed store instance from context.
*/
useStore: () => Store<UnionSliceTarget<Slices>, Slices>;
useStore: () => Store<UnionFeatureTarget<Features>, Features>;
/**
* Returns a snapshot of the store state.
* Re-renders when state changes.
*/
useSnapshot: () => UnionSliceState<Slices>;
useSnapshot: () => UnionFeatureState<Features>;
/**
* Returns the request map or a specific request by name.
*/
useRequest: {
(): UnionSliceRequests<Slices>;
<Name extends keyof UnionSliceRequests<Slices>>(name: Name): UnionSliceRequests<Slices>[Name];
(): UnionFeatureRequests<Features>;
<Name extends keyof UnionFeatureRequests<Features>>(name: Name): UnionFeatureRequests<Features>[Name];
};
/**
* Subscribes to task state changes.
* Returns the current tasks map from the queue.
*/
useTasks: () => TasksRecord<UnionSliceTasks<Slices>>;
useTasks: () => TasksRecord<UnionFeatureTasks<Features>>;
/**
* Creates a new store instance.
* Useful for imperative access or creating a store before render.
*/
create: () => Store<UnionSliceTarget<Slices>, Slices>;
create: () => Store<UnionFeatureTarget<Features>, Features>;
}
// ----------------------------------------
@@ -83,22 +83,22 @@ export interface CreateStoreResult<Slices extends AnySlice[]> {
/**
* Creates a store factory that returns a Provider and typed hooks.
*
* @param config - Store configuration including slices and optional lifecycle hooks
* @param config - Store configuration including features and optional lifecycle hooks
* @returns An object containing Provider, hooks, and a create function
*
* @example
* ```tsx
* const { Provider, useStore, useSnapshot, useRequest, useTasks, create } = createStore({
* slices: [playbackSlice, presentationSlice],
* features: [playbackFeature, presentationFeature],
* });
* ```
*/
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>;
export function createStore<Features extends AnyFeature[]>(config: CreateStoreConfig<Features>): CreateStoreResult<Features> {
type Target = UnionFeatureTarget<Features>;
type State = UnionFeatureState<Features>;
type Requests = UnionFeatureRequests<Features>;
type Tasks = UnionFeatureTasks<Features>;
type StoreType = Store<Target, Features>;
function create(): StoreType {
return new Store(config);
@@ -112,7 +112,7 @@ export function createStore<Slices extends AnySlice[]>(config: CreateStoreConfig
* 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 {
function Provider({ children, store: providedStore, inherit = false }: ProviderProps<Features>): ReactNode {
const parentStore = useParentStore();
const shouldInherit = inherit && !isNull(parentStore);
@@ -172,7 +172,7 @@ export function createStore<Slices extends AnySlice[]>(config: CreateStoreConfig
Provider,
useStore,
useSnapshot,
useRequest: useRequest as CreateStoreResult<Slices>['useRequest'],
useRequest: useRequest as CreateStoreResult<Features>['useRequest'],
useTasks,
create,
};
@@ -1,6 +1,6 @@
import { noop } from '@videojs/utils/function';
import { createSlice } from '../../../core/slice';
import { createFeature } from '../../../core/feature';
import { createStore as createCoreStore } from '../../../core/store';
// Shared mock target for synchronous tests
@@ -9,8 +9,8 @@ export class MockMedia extends EventTarget {
muted = false;
}
// Shared slice for synchronous tests
export const audioSlice = createSlice<MockMedia>()({
// Shared feature for synchronous tests
export const audioFeature = createFeature<MockMedia>()({
initialState: { volume: 1, muted: false },
getSnapshot: ({ target }) => ({
volume: target.volume,
@@ -37,7 +37,7 @@ export const audioSlice = createSlice<MockMedia>()({
});
export function createTestStore() {
const store = createCoreStore({ slices: [audioSlice] });
const store = createCoreStore({ features: [audioFeature] });
const target = new MockMedia();
store.attach(target);
return { store, target };
@@ -49,7 +49,7 @@ export class AsyncMockMedia extends EventTarget {
muted = false;
}
export const asyncAudioSlice = createSlice<AsyncMockMedia>()({
export const asyncAudioFeature = createFeature<AsyncMockMedia>()({
initialState: { volume: 1, muted: false },
getSnapshot: ({ target }) => ({
volume: target.volume,
@@ -96,7 +96,7 @@ export const asyncAudioSlice = createSlice<AsyncMockMedia>()({
export function createAsyncTestStore() {
const store = createCoreStore({
slices: [asyncAudioSlice],
features: [asyncAudioFeature],
onError: noop,
});
@@ -106,8 +106,8 @@ export function createAsyncTestStore() {
return { store, target };
}
/** Slice with custom keys (name !== key) for testing superseding behavior. */
export const customKeySlice = createSlice<MockMedia>()({
/** Feature with custom keys (name !== key) for testing superseding behavior. */
export const customKeyFeature = createFeature<MockMedia>()({
initialState: { volume: 1, muted: false },
getSnapshot: ({ target }) => ({
volume: target.volume,
@@ -146,7 +146,7 @@ export const customKeySlice = createSlice<MockMedia>()({
export function createCustomKeyTestStore() {
const store = createCoreStore({
slices: [customKeySlice],
features: [customKeyFeature],
onError: noop,
});
@@ -4,7 +4,7 @@ import { renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { createSlice } from '../../core/slice';
import { createFeature } from '../../core/feature';
import { createStore as createCoreStore } from '../../core/store';
import { StoreContextProvider, useStoreContext } from '../context';
@@ -14,7 +14,7 @@ describe('context', () => {
volume = 1;
}
const audioSlice = createSlice<MockMedia>()({
const audioFeature = createFeature<MockMedia>()({
initialState: { volume: 1 },
getSnapshot: ({ target }) => ({ volume: target.volume }),
subscribe: () => {},
@@ -29,7 +29,7 @@ describe('context', () => {
});
it('returns store from context', () => {
const store = createCoreStore({ slices: [audioSlice] });
const store = createCoreStore({ features: [audioFeature] });
const { result } = renderHook(() => useStoreContext(), {
wrapper: ({ children }: { children: ReactNode }) => (
@@ -4,7 +4,7 @@ import { act, renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { createSlice } from '../../core/slice';
import { createFeature } from '../../core/feature';
import { createStore } from '../create-store';
describe('createStore', () => {
@@ -14,7 +14,7 @@ describe('createStore', () => {
muted = false;
}
const audioSlice = createSlice<MockMedia>()({
const audioFeature = createFeature<MockMedia>()({
initialState: { volume: 1, muted: false },
getSnapshot: ({ target }) => ({
volume: target.volume,
@@ -37,7 +37,7 @@ describe('createStore', () => {
describe('create', () => {
it('creates a store instance', () => {
const { create } = createStore({ slices: [audioSlice] });
const { create } = createStore({ features: [audioFeature] });
const store = create();
@@ -48,7 +48,7 @@ describe('createStore', () => {
describe('provider', () => {
it('creates store on mount', () => {
const { Provider, useStore } = createStore({ slices: [audioSlice] });
const { Provider, useStore } = createStore({ features: [audioFeature] });
const { result } = renderHook(() => useStore(), {
wrapper: ({ children }: { children: ReactNode }) => <Provider>{children}</Provider>,
@@ -59,7 +59,7 @@ describe('createStore', () => {
});
it('destroys store on unmount', () => {
const { Provider, useStore } = createStore({ slices: [audioSlice] });
const { Provider, useStore } = createStore({ features: [audioFeature] });
const { result, unmount } = renderHook(() => useStore(), {
wrapper: ({ children }: { children: ReactNode }) => <Provider>{children}</Provider>,
@@ -74,7 +74,7 @@ describe('createStore', () => {
});
it('uses provided store prop without destroying on unmount', () => {
const { Provider, useStore, create } = createStore({ slices: [audioSlice] });
const { Provider, useStore, create } = createStore({ features: [audioFeature] });
const providedStore = create();
const { result, unmount } = renderHook(() => useStore(), {
@@ -90,7 +90,7 @@ describe('createStore', () => {
});
it('inherits store from parent when inherit=true', () => {
const { Provider, useStore, create } = createStore({ slices: [audioSlice] });
const { Provider, useStore, create } = createStore({ features: [audioFeature] });
const parentStore = create();
const { result } = renderHook(() => useStore(), {
@@ -105,7 +105,7 @@ describe('createStore', () => {
});
it('creates isolated store by default', () => {
const { Provider, useStore, create } = createStore({ slices: [audioSlice] });
const { Provider, useStore, create } = createStore({ features: [audioFeature] });
const parentStore = create();
const { result } = renderHook(() => useStore(), {
@@ -122,7 +122,7 @@ describe('createStore', () => {
it('sets displayName on Provider', () => {
const { Provider } = createStore({
slices: [audioSlice],
features: [audioFeature],
displayName: 'TestStore',
});
@@ -132,7 +132,7 @@ describe('createStore', () => {
describe('useStore', () => {
it('returns the store from context', () => {
const { Provider, useStore, create } = createStore({ slices: [audioSlice] });
const { Provider, useStore, create } = createStore({ features: [audioFeature] });
const store = create();
const { result } = renderHook(() => useStore(), {
@@ -145,7 +145,7 @@ describe('createStore', () => {
describe('useSnapshot', () => {
it('returns state from context store', () => {
const { Provider, useSnapshot, create } = createStore({ slices: [audioSlice] });
const { Provider, useSnapshot, create } = createStore({ features: [audioFeature] });
const store = create();
const target = new MockMedia();
store.attach(target);
@@ -158,7 +158,7 @@ describe('createStore', () => {
});
it('updates when state changes', async () => {
const { Provider, useSnapshot, create } = createStore({ slices: [audioSlice] });
const { Provider, useSnapshot, create } = createStore({ features: [audioFeature] });
const store = create();
const target = new MockMedia();
store.attach(target);
@@ -180,7 +180,7 @@ describe('createStore', () => {
describe('useRequest', () => {
it('returns request map from context store', () => {
const { Provider, useRequest, create } = createStore({ slices: [audioSlice] });
const { Provider, useRequest, create } = createStore({ features: [audioFeature] });
const store = create();
const { result } = renderHook(() => useRequest(), {
@@ -191,7 +191,7 @@ describe('createStore', () => {
});
it('returns request by name', () => {
const { Provider, useRequest, create } = createStore({ slices: [audioSlice] });
const { Provider, useRequest, create } = createStore({ features: [audioFeature] });
const store = create();
const { result } = renderHook(() => useRequest('setVolume'), {
@@ -204,7 +204,7 @@ describe('createStore', () => {
describe('useTasks', () => {
it('returns tasks from context store', () => {
const { Provider, useTasks, create } = createStore({ slices: [audioSlice] });
const { Provider, useTasks, create } = createStore({ features: [audioFeature] });
const store = create();
const { result } = renderHook(() => useTasks(), {