refactor(store): simplify create store implementations (#361)

This commit is contained in:
rahim
2026-01-31 19:03:43 +11:00
committed by GitHub
parent f620b1782a
commit 9c725803a6
12 changed files with 69 additions and 103 deletions
+9 -9
View File
@@ -7,7 +7,7 @@ import type { AnyFeature, UnionFeatureRequests, UnionFeatureState, UnionFeatureT
import type { StoreConfig, StoreConsumer, StoreProvider } from '../core/store';
import { Store } from '../core/store';
import { createStoreAttachMixin, createStoreMixin, createStoreProviderMixin } from './mixins';
import { createContainerMixin, createProviderMixin, createStoreMixin } from './mixins';
export const contextKey = Symbol('@videojs/store');
@@ -37,10 +37,10 @@ export interface CreateStoreResult<Features extends AnyFeature[]> {
*
* @example
* ```ts
* class MyProvider extends StoreProviderMixin(LitElement) {}
* class MyProvider extends ProviderMixin(LitElement) {}
* ```
*/
StoreProviderMixin: <T extends Constructor<ReactiveElement>>(Base: T) => T & Constructor<StoreProvider<Features>>;
ProviderMixin: <T extends Constructor<ReactiveElement>>(Base: T) => T & Constructor<StoreProvider<Features>>;
/**
* Mixin that auto-attaches slotted media elements (requires store from context).
@@ -49,10 +49,10 @@ export interface CreateStoreResult<Features extends AnyFeature[]> {
*
* @example
* ```ts
* class MyControls extends StoreAttachMixin(LitElement) {}
* class MyControls extends ContainerMixin(LitElement) {}
* ```
*/
StoreAttachMixin: <T extends Constructor<ReactiveElement>>(Base: T) => T & Constructor<StoreConsumer<Features>>;
ContainerMixin: <T extends Constructor<ReactiveElement>>(Base: T) => T & Constructor<StoreConsumer<Features>>;
/**
* Context for consuming store in controllers.
@@ -156,8 +156,8 @@ export function createStore<Features extends AnyFeature[]>(
return new Store(config);
}
const StoreProviderMixin = createStoreProviderMixin<Features>(context, create);
const StoreAttachMixin = createStoreAttachMixin<Features>(context);
const ProviderMixin = createProviderMixin<Features>(context, create);
const ContainerMixin = createContainerMixin<Features>(context);
const StoreMixin = createStoreMixin<Features>(context, create);
class StoreController {
@@ -208,8 +208,8 @@ export function createStore<Features extends AnyFeature[]>(
return {
StoreMixin,
StoreProviderMixin,
StoreAttachMixin,
ProviderMixin,
ContainerMixin,
context,
create,
StoreController,
+2 -2
View File
@@ -15,9 +15,9 @@ export { createStore } from './create-store';
// Mixin factories (for advanced use cases)
export {
createStoreAttachMixin,
createContainerMixin,
createProviderMixin,
createStoreMixin,
createStoreProviderMixin,
} from './mixins';
export type { StoreSource } from './store-accessor';
// StoreAccessor (for custom controllers)
@@ -5,8 +5,8 @@ import type { AnyFeature, UnionFeatureTarget } from '../../core/feature';
import type { Store, StoreProvider } from '../../core/store';
import { createStoreAttachMixin } from './attach-mixin';
import { createStoreProviderMixin } from './provider-mixin';
import { createContainerMixin } from './container-mixin';
import { createProviderMixin } from './provider-mixin';
/**
* Creates a combined mixin that both provides a store and auto-attaches media elements.
@@ -29,13 +29,13 @@ export function createStoreMixin<Features extends AnyFeature[]>(
context: Context<unknown, Store<UnionFeatureTarget<Features>, Features>>,
factory: () => Store<UnionFeatureTarget<Features>, Features>
): Mixin<ReactiveElement, StoreProvider<Features>> {
const ProviderMixin = createStoreProviderMixin<Features>(context, factory);
const AttachMixin = createStoreAttachMixin<Features>(context);
const ProviderMixin = createProviderMixin<Features>(context, factory);
const ContainerMixin = createContainerMixin<Features>(context);
return <Base extends Constructor<ReactiveElement>>(BaseClass: Base) => {
// ProviderMixin wraps AttachMixin so during connectedCallback:
// 1. ProviderMixin runs first (provides store via context)
// 2. AttachMixin runs second (consumes store from context)
return ProviderMixin(AttachMixin(BaseClass));
return ProviderMixin(ContainerMixin(BaseClass));
};
}
@@ -20,12 +20,12 @@ import type { Store, StoreConsumer } from '../../core/store';
*
* @example
* ```ts
* const { StoreAttachMixin } = createStore({ features: [playbackFeature] });
* const { ContainerMixin } = createStore({ features: [playbackFeature] });
*
* class MyControls extends StoreAttachMixin(LitElement) {}
* class MyControls extends ContainerMixin(LitElement) {}
* ```
*/
export function createStoreAttachMixin<Features extends AnyFeature[]>(
export function createContainerMixin<Features extends AnyFeature[]>(
context: Context<unknown, Store<UnionFeatureTarget<Features>, Features>>
): Mixin<ReactiveElement, StoreConsumer<Features>> {
type ConsumedStore = Store<UnionFeatureTarget<Features>, Features>;
+2 -2
View File
@@ -1,3 +1,3 @@
export { createStoreAttachMixin } from './attach-mixin';
export { createStoreMixin } from './combined-mixin';
export { createStoreProviderMixin } from './provider-mixin';
export { createContainerMixin } from './container-mixin';
export { createProviderMixin } from './provider-mixin';
@@ -16,18 +16,18 @@ import type { Store, StoreProvider } from '../../core/store';
*
* @example
* ```ts
* const { StoreProviderMixin } = createStore({
* const { ProviderMixin } = createStore({
* features: [playbackFeature]
* });
*
* class MyPlayer extends StoreProviderMixin(LitElement) {
* class MyPlayer extends ProviderMixin(LitElement) {
* render() {
* return html`<slot></slot>`;
* }
* }
* ```
*/
export function createStoreProviderMixin<Features extends AnyFeature[]>(
export function createProviderMixin<Features extends AnyFeature[]>(
context: Context<unknown, Store<UnionFeatureTarget<Features>, Features>>,
factory: () => Store<UnionFeatureTarget<Features>, Features>
): <Base extends Constructor<ReactiveElement>>(BaseClass: Base) => Base & Constructor<StoreProvider<Features>> {
@@ -4,12 +4,12 @@ import { createLitTestStore, setupDomCleanup, TestBaseElement, uniqueTag } from
setupDomCleanup();
describe('createStoreAttachMixin', () => {
describe('createContainerMixin', () => {
it('exposes store property (initially null without context)', async () => {
const { StoreAttachMixin } = createLitTestStore();
const tagName = uniqueTag('test-attach-standalone');
const { ContainerMixin } = createLitTestStore();
const tagName = uniqueTag('test-container-standalone');
class TestElement extends StoreAttachMixin(TestBaseElement) {}
class TestElement extends ContainerMixin(TestBaseElement) {}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
@@ -21,9 +21,9 @@ describe('createStoreAttachMixin', () => {
});
it('can be applied to TestBaseElement', () => {
const { StoreAttachMixin } = createLitTestStore();
const { ContainerMixin } = createLitTestStore();
class MixedElement extends StoreAttachMixin(TestBaseElement) {}
class MixedElement extends ContainerMixin(TestBaseElement) {}
expect(MixedElement.prototype).toBeInstanceOf(TestBaseElement);
});
@@ -4,12 +4,12 @@ import { createLitTestStore, setupDomCleanup, TestBaseElement, uniqueTag } from
setupDomCleanup();
describe('createStoreProviderMixin', () => {
describe('createProviderMixin', () => {
it('creates store lazily on first access', async () => {
const { StoreProviderMixin } = createLitTestStore();
const { ProviderMixin } = createLitTestStore();
const tagName = uniqueTag('test-provider');
class TestElement extends StoreProviderMixin(TestBaseElement) {}
class TestElement extends ProviderMixin(TestBaseElement) {}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
@@ -21,10 +21,10 @@ describe('createStoreProviderMixin', () => {
});
it('reuses same store instance', async () => {
const { StoreProviderMixin } = createLitTestStore();
const { ProviderMixin } = createLitTestStore();
const tagName = uniqueTag('test-provider-reuse');
class TestElement extends StoreProviderMixin(TestBaseElement) {}
class TestElement extends ProviderMixin(TestBaseElement) {}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
@@ -38,10 +38,10 @@ describe('createStoreProviderMixin', () => {
});
it('destroys owned store on disconnect', async () => {
const { StoreProviderMixin } = createLitTestStore();
const { ProviderMixin } = createLitTestStore();
const tagName = uniqueTag('test-provider-destroy');
class TestElement extends StoreProviderMixin(TestBaseElement) {}
class TestElement extends ProviderMixin(TestBaseElement) {}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
@@ -57,10 +57,10 @@ describe('createStoreProviderMixin', () => {
});
it('allows setting custom store via setter', async () => {
const { StoreProviderMixin, create } = createLitTestStore();
const { ProviderMixin, create } = createLitTestStore();
const tagName = uniqueTag('test-provider-setter');
class TestElement extends StoreProviderMixin(TestBaseElement) {}
class TestElement extends ProviderMixin(TestBaseElement) {}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
@@ -74,10 +74,10 @@ describe('createStoreProviderMixin', () => {
});
it('does not destroy externally provided store on disconnect', async () => {
const { StoreProviderMixin, create } = createLitTestStore();
const { ProviderMixin, create } = createLitTestStore();
const tagName = uniqueTag('test-provider-external');
class TestElement extends StoreProviderMixin(TestBaseElement) {}
class TestElement extends ProviderMixin(TestBaseElement) {}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
@@ -93,10 +93,10 @@ describe('createStoreProviderMixin', () => {
});
it('destroys old owned store when setting new store', async () => {
const { StoreProviderMixin, create } = createLitTestStore();
const { ProviderMixin, create } = createLitTestStore();
const tagName = uniqueTag('test-provider-replace');
class TestElement extends StoreProviderMixin(TestBaseElement) {}
class TestElement extends ProviderMixin(TestBaseElement) {}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
@@ -12,18 +12,18 @@ describe('mixin types', () => {
expectTypeOf<Instance>().toHaveProperty('store');
});
it('storeProviderMixin adds store property', () => {
const { StoreProviderMixin } = createLitTestStore();
const _MixedElement = StoreProviderMixin(TestBaseElement);
it('providerMixin adds store property', () => {
const { ProviderMixin } = createLitTestStore();
const _MixedElement = ProviderMixin(TestBaseElement);
type Instance = InstanceType<typeof _MixedElement>;
// Verify store property exists on the mixed type
expectTypeOf<Instance>().toHaveProperty('store');
});
it('storeAttachMixin adds store property', () => {
const { StoreAttachMixin } = createLitTestStore();
const _MixedElement = StoreAttachMixin(TestBaseElement);
it('containerMixin adds store property', () => {
const { ContainerMixin } = createLitTestStore();
const _MixedElement = ContainerMixin(TestBaseElement);
type Instance = InstanceType<typeof _MixedElement>;
// Verify store property exists on the mixed type
@@ -76,26 +76,26 @@ describe('createStore', () => {
expect(typeof StoreMixin).toBe('function');
});
it('returns StoreProviderMixin', () => {
const { StoreProviderMixin } = createStore({ features: [audioFeature] });
it('returns ProviderMixin', () => {
const { ProviderMixin } = createStore({ features: [audioFeature] });
expect(typeof StoreProviderMixin).toBe('function');
expect(typeof ProviderMixin).toBe('function');
});
it('returns StoreAttachMixin', () => {
const { StoreAttachMixin } = createStore({ features: [audioFeature] });
it('returns ContainerMixin', () => {
const { ContainerMixin } = createStore({ features: [audioFeature] });
expect(typeof StoreAttachMixin).toBe('function');
expect(typeof ContainerMixin).toBe('function');
});
it('mixins can be applied to TestBaseElement', () => {
const { StoreMixin, StoreProviderMixin, StoreAttachMixin } = createStore({
const { StoreMixin, ProviderMixin, ContainerMixin } = createStore({
features: [audioFeature],
});
const Mixed1 = StoreMixin(TestBaseElement);
const Mixed2 = StoreProviderMixin(TestBaseElement);
const Mixed3 = StoreAttachMixin(TestBaseElement);
const Mixed2 = ProviderMixin(TestBaseElement);
const Mixed3 = ContainerMixin(TestBaseElement);
expect(Mixed1.prototype).toBeInstanceOf(TestBaseElement);
expect(Mixed2.prototype).toBeInstanceOf(TestBaseElement);
@@ -108,8 +108,8 @@ describe('createStore', () => {
const result = createStore({ features: [audioFeature] });
expect(result).toHaveProperty('StoreMixin');
expect(result).toHaveProperty('StoreProviderMixin');
expect(result).toHaveProperty('StoreAttachMixin');
expect(result).toHaveProperty('ProviderMixin');
expect(result).toHaveProperty('ContainerMixin');
expect(result).toHaveProperty('context');
expect(result).toHaveProperty('create');
expect(result).toHaveProperty('StoreController');
-9
View File
@@ -24,15 +24,6 @@ export function useStoreContext(): AnyStore {
return store;
}
/**
* Internal hook to get parent store from context.
* Returns null if no parent Provider exists.
* Used by Provider to implement the `inherit` prop.
*/
export function useParentStore(): AnyStore | null {
return useContext(StoreContext);
}
/**
* Internal provider component that wraps children with store context.
*/
+9 -34
View File
@@ -1,11 +1,12 @@
import { isNull, isUndefined } from '@videojs/utils/predicate';
import { isUndefined } from '@videojs/utils/predicate';
import type { FC, ReactNode } from 'react';
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
import { useEffect, useState } from 'react';
import type { AnyFeature, UnionFeatureRequests, UnionFeatureState, UnionFeatureTarget } from '../core/feature';
import type { StoreConfig } from '../core/store';
import { Store } from '../core/store';
import { StoreContextProvider, useParentStore, useStoreContext } from './context';
import { StoreContextProvider, useStoreContext } from './context';
import { useStore as useStoreBase } from './hooks/use-store';
// ----------------------------------------
// Types
@@ -25,12 +26,6 @@ export interface ProviderProps<Features extends AnyFeature[]> {
* The Provider will NOT destroy this store on unmount.
*/
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.
* Defaults to false (isolated store).
*/
inherit?: boolean;
}
export type UseStoreResult<Features extends AnyFeature[]> = UnionFeatureState<Features> &
@@ -83,29 +78,19 @@ export function createStore<Features extends AnyFeature[]>(
/**
* 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
* If `store` prop is provided, uses that store (no cleanup on unmount).
* Otherwise, creates a new store and destroys it on unmount.
*/
function Provider({ children, store: providedStore, inherit = false }: ProviderProps<Features>): ReactNode {
const parentStore = useParentStore();
const shouldInherit = inherit && !isNull(parentStore);
function Provider({ children, store: providedStore }: ProviderProps<Features>): ReactNode {
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;
const isOwner = isUndefined(providedStore);
useEffect(() => {
if (isOwner) {
@@ -125,17 +110,7 @@ export function createStore<Features extends AnyFeature[]>(
function useStore(): UseStoreResult<Features> {
const store = useStoreContext();
const state = useSyncExternalStore(
(cb) => store.subscribe(cb),
() => store.state,
() => store.state
);
return useMemo(
() => ({ ...state, ...(store.request as object) }) as UseStoreResult<Features>,
[state, store.request]
);
return useStoreBase(store) as UseStoreResult<Features>;
}
return {