mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(packages): move store attach lifecycle to provider (#975)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,9 @@ export {
|
||||
Container,
|
||||
type ContainerProps,
|
||||
type PlayerContextValue,
|
||||
useContainerAttach,
|
||||
useMedia,
|
||||
useMediaRegistration,
|
||||
useMediaAttach,
|
||||
usePlayer,
|
||||
usePlayerContext,
|
||||
} from './player/context';
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import type { AudioHTMLAttributes } from 'react';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { useMediaRegistration } from '../player/context';
|
||||
import { useMediaAttach } from '../player/context';
|
||||
import { useComposedRefs } from '../utils/use-composed-refs';
|
||||
|
||||
export interface AudioProps extends AudioHTMLAttributes<HTMLAudioElement> {}
|
||||
|
||||
export const Audio = forwardRef<HTMLAudioElement, AudioProps>(function Audio({ children, ...props }, ref) {
|
||||
const setMedia = useMediaRegistration();
|
||||
|
||||
const mediaRef = useCallback(
|
||||
(el: HTMLAudioElement | null) => {
|
||||
setMedia?.(el);
|
||||
},
|
||||
[setMedia]
|
||||
);
|
||||
|
||||
const composedRef = useComposedRefs(ref, mediaRef);
|
||||
const setMedia = useMediaAttach();
|
||||
const composedRef = useComposedRefs(ref, setMedia);
|
||||
|
||||
return (
|
||||
<audio ref={composedRef} {...props}>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { VideoHTMLAttributes } from 'react';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
|
||||
import { useMediaRegistration } from '../../player/context';
|
||||
import { useMediaAttach } from '../../player/context';
|
||||
import { useComposedRefs } from '../../utils/use-composed-refs';
|
||||
|
||||
export interface BackgroundVideoProps extends VideoHTMLAttributes<HTMLVideoElement> {}
|
||||
@@ -12,7 +12,7 @@ export const BackgroundVideo = forwardRef<HTMLVideoElement, BackgroundVideoProps
|
||||
{ children, ...props },
|
||||
ref
|
||||
) {
|
||||
const setMedia = useMediaRegistration();
|
||||
const setMedia = useMediaAttach();
|
||||
|
||||
const mediaRef = useCallback(
|
||||
(el: HTMLVideoElement | null) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DashMedia } from '@videojs/core/dom/media/dash';
|
||||
import type { PropsWithChildren, VideoHTMLAttributes } from 'react';
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
import { useMediaRegistration } from '../../player/context';
|
||||
import { useMediaAttach } from '../../player/context';
|
||||
import { attachMediaElement } from '../../utils/attach-media-element';
|
||||
import { mediaProps } from '../../utils/media-props';
|
||||
import { useComposedRefs } from '../../utils/use-composed-refs';
|
||||
@@ -11,13 +11,14 @@ export type DashVideoProps = PropsWithChildren<VideoHTMLAttributes<HTMLVideoElem
|
||||
|
||||
export const DashVideo = forwardRef<HTMLVideoElement, DashVideoProps>(({ children, ...props }, ref) => {
|
||||
const mediaApi = useMemo(() => new DashMedia(), []);
|
||||
const setMedia = useMediaRegistration();
|
||||
const setMedia = useMediaAttach();
|
||||
|
||||
useDestroy(mediaApi, () => {
|
||||
setMedia?.(mediaApi);
|
||||
});
|
||||
|
||||
const composedRef = useComposedRefs(attachMediaElement(mediaApi), ref);
|
||||
|
||||
return (
|
||||
<video ref={composedRef} {...mediaProps(mediaApi, props)}>
|
||||
{children}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HlsMedia } from '@videojs/core/dom/media/hls';
|
||||
import type { PropsWithChildren, VideoHTMLAttributes } from 'react';
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
import { useMediaRegistration } from '../../player/context';
|
||||
import { useMediaAttach } from '../../player/context';
|
||||
import { attachMediaElement } from '../../utils/attach-media-element';
|
||||
import { mediaProps } from '../../utils/media-props';
|
||||
import { useComposedRefs } from '../../utils/use-composed-refs';
|
||||
@@ -11,13 +11,14 @@ export type HlsVideoProps = PropsWithChildren<VideoHTMLAttributes<HTMLVideoEleme
|
||||
|
||||
export const HlsVideo = forwardRef<HTMLVideoElement, HlsVideoProps>(({ children, ...props }, ref) => {
|
||||
const mediaApi = useMemo(() => new HlsMedia(), []);
|
||||
const setMedia = useMediaRegistration();
|
||||
const setMedia = useMediaAttach();
|
||||
|
||||
useDestroy(mediaApi, () => {
|
||||
setMedia?.(mediaApi);
|
||||
});
|
||||
|
||||
const composedRef = useComposedRefs(attachMediaElement(mediaApi), ref);
|
||||
|
||||
return (
|
||||
<video ref={composedRef} {...mediaProps(mediaApi, props)}>
|
||||
{children}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SimpleHlsMedia } from '@videojs/core/dom/media/simple-hls';
|
||||
import type { PropsWithChildren, VideoHTMLAttributes } from 'react';
|
||||
import { forwardRef, useEffect, useMemo } from 'react';
|
||||
import { useMediaRegistration } from '../../player/context';
|
||||
import { useMediaAttach } from '../../player/context';
|
||||
import { attachMediaElement } from '../../utils/attach-media-element';
|
||||
import { mediaProps } from '../../utils/media-props';
|
||||
import { useComposedRefs } from '../../utils/use-composed-refs';
|
||||
@@ -12,13 +12,14 @@ export type SimpleHlsVideoProps = PropsWithChildren<VideoHTMLAttributes<HTMLVide
|
||||
|
||||
export const SimpleHlsVideo = forwardRef<HTMLVideoElement, SimpleHlsVideoProps>(({ children, ...props }, ref) => {
|
||||
const mediaApi = useMemo(() => new SimpleHlsMedia(), []);
|
||||
const setMedia = useMediaRegistration();
|
||||
const setMedia = useMediaAttach();
|
||||
|
||||
useEffect(() => {
|
||||
setMedia?.(mediaApi);
|
||||
}, [mediaApi, setMedia]);
|
||||
|
||||
const composedRef = useComposedRefs(attachMediaElement(mediaApi), ref);
|
||||
|
||||
return (
|
||||
<video ref={composedRef} {...mediaProps(mediaApi, props)}>
|
||||
{children}
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('Audio', () => {
|
||||
it('calls setMedia on mount', () => {
|
||||
const setMedia = vi.fn();
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia };
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia, setContainer: vi.fn() };
|
||||
|
||||
render(<Audio />, { wrapper: createWrapper(value) });
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('Audio', () => {
|
||||
it('calls setMedia with null on unmount', () => {
|
||||
const setMedia = vi.fn();
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia };
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia, setContainer: vi.fn() };
|
||||
|
||||
const { unmount } = render(<Audio />, { wrapper: createWrapper(value) });
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('Audio', () => {
|
||||
it('forwards ref while also registering media', () => {
|
||||
const setMedia = vi.fn();
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia };
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia, setContainer: vi.fn() };
|
||||
|
||||
const ref = createRef<HTMLAudioElement>();
|
||||
render(<Audio ref={ref} />, { wrapper: createWrapper(value) });
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('Video', () => {
|
||||
it('calls setMedia on mount', () => {
|
||||
const setMedia = vi.fn();
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia };
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia, setContainer: vi.fn() };
|
||||
|
||||
render(<Video />, { wrapper: createWrapper(value) });
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('Video', () => {
|
||||
it('calls setMedia with null on unmount', () => {
|
||||
const setMedia = vi.fn();
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia };
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia, setContainer: vi.fn() };
|
||||
|
||||
const { unmount } = render(<Video />, { wrapper: createWrapper(value) });
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('Video', () => {
|
||||
it('forwards ref while also registering media', () => {
|
||||
const setMedia = vi.fn();
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia };
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia, setContainer: vi.fn() };
|
||||
|
||||
const ref = createRef<HTMLVideoElement>();
|
||||
render(<Video ref={ref} />, { wrapper: createWrapper(value) });
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import type { VideoHTMLAttributes } from 'react';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { useMediaRegistration } from '../player/context';
|
||||
import { useMediaAttach } from '../player/context';
|
||||
import { useComposedRefs } from '../utils/use-composed-refs';
|
||||
|
||||
export interface VideoProps extends VideoHTMLAttributes<HTMLVideoElement> {}
|
||||
|
||||
export const Video = forwardRef<HTMLVideoElement, VideoProps>(function Video({ children, ...props }, ref) {
|
||||
const setMedia = useMediaRegistration();
|
||||
|
||||
const mediaRef = useCallback(
|
||||
(el: HTMLVideoElement | null) => {
|
||||
setMedia?.(el);
|
||||
},
|
||||
[setMedia]
|
||||
);
|
||||
|
||||
const composedRef = useComposedRefs(ref, mediaRef);
|
||||
const setMedia = useMediaAttach();
|
||||
const composedRef = useComposedRefs(ref, setMedia);
|
||||
|
||||
return (
|
||||
<video ref={composedRef} {...props}>
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface PlayerContextValue {
|
||||
store: UnknownStore;
|
||||
media: Media | null;
|
||||
setMedia: Dispatch<SetStateAction<Media | null>>;
|
||||
setContainer: Dispatch<SetStateAction<HTMLElement | null>>;
|
||||
}
|
||||
|
||||
const PlayerContext = createContext<PlayerContextValue | null>(null);
|
||||
@@ -79,25 +80,31 @@ export function useMedia(): Media | null {
|
||||
return media;
|
||||
}
|
||||
|
||||
/** Access the media registration setter for connecting a media element to the player. */
|
||||
export function useMediaRegistration(): Dispatch<SetStateAction<Media | null>> | undefined {
|
||||
/** Access the media attach setter for connecting a media element to the player. */
|
||||
export function useMediaAttach(): Dispatch<SetStateAction<Media | null>> | undefined {
|
||||
const ctx = useContext(PlayerContext);
|
||||
return ctx?.setMedia;
|
||||
}
|
||||
|
||||
/** Access the container attach setter for connecting a container element to the player. */
|
||||
export function useContainerAttach(): Dispatch<SetStateAction<HTMLElement | null>> | undefined {
|
||||
const ctx = useContext(PlayerContext);
|
||||
return ctx?.setContainer;
|
||||
}
|
||||
|
||||
export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Container = forwardRef<HTMLDivElement, ContainerProps>(function Container({ children, ...props }, ref) {
|
||||
const { store, media } = usePlayerContext();
|
||||
const setContainer = useContainerAttach();
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
const composedRef = useComposedRefs(ref, internalRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (!media) return;
|
||||
return store.attach({ media, container: internalRef.current });
|
||||
}, [media, store]);
|
||||
setContainer?.(internalRef.current);
|
||||
return () => setContainer?.(null);
|
||||
}, [setContainer]);
|
||||
|
||||
return (
|
||||
<div ref={composedRef} {...props}>
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { InferStoreState } from '@videojs/store';
|
||||
import { combine, createStore } from '@videojs/store';
|
||||
import { useStore } from '@videojs/store/react';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useDestroy } from '../utils/use-destroy';
|
||||
import { Container, PlayerContextProvider, useMedia, usePlayerContext } from './context';
|
||||
@@ -71,10 +71,16 @@ export function createPlayer(config: CreatePlayerConfig<AnyPlayerFeature[]>): Cr
|
||||
function Provider({ children }: ProviderProps): ReactNode {
|
||||
const [store] = useState(() => createStore<PlayerTarget>()(combine(...config.features)));
|
||||
const [media, setMedia] = useState<Media | null>(null);
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null);
|
||||
|
||||
useDestroy(store);
|
||||
|
||||
return <PlayerContextProvider value={{ store, media, setMedia }}>{children}</PlayerContextProvider>;
|
||||
useEffect(() => {
|
||||
if (!media) return;
|
||||
return store.attach({ media, container });
|
||||
}, [media, container, store]);
|
||||
|
||||
return <PlayerContextProvider value={{ store, media, setMedia, setContainer }}>{children}</PlayerContextProvider>;
|
||||
}
|
||||
|
||||
if (__DEV__ && config.displayName) {
|
||||
|
||||
@@ -7,8 +7,9 @@ import {
|
||||
Container,
|
||||
PlayerContextProvider,
|
||||
type PlayerContextValue,
|
||||
useContainerAttach,
|
||||
useMedia,
|
||||
useMediaRegistration,
|
||||
useMediaAttach,
|
||||
useOptionalPlayer,
|
||||
usePlayer,
|
||||
usePlayerContext,
|
||||
@@ -20,6 +21,16 @@ function createWrapper(value: PlayerContextValue) {
|
||||
};
|
||||
}
|
||||
|
||||
function createContextValue(overrides?: Partial<PlayerContextValue>): PlayerContextValue {
|
||||
return {
|
||||
store: createMockStore() as any,
|
||||
media: null,
|
||||
setMedia: vi.fn(),
|
||||
setContainer: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('usePlayerContext', () => {
|
||||
it('throws outside Provider', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
@@ -33,7 +44,7 @@ describe('usePlayerContext', () => {
|
||||
|
||||
it('returns context value inside Provider', () => {
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia: vi.fn() };
|
||||
const value = createContextValue({ store: store as any });
|
||||
|
||||
const { result } = renderHook(() => usePlayerContext(), {
|
||||
wrapper: createWrapper(value),
|
||||
@@ -44,18 +55,17 @@ describe('usePlayerContext', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useMediaRegistration', () => {
|
||||
describe('useMediaAttach', () => {
|
||||
it('returns undefined outside Provider', () => {
|
||||
const { result } = renderHook(() => useMediaRegistration());
|
||||
const { result } = renderHook(() => useMediaAttach());
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns setMedia inside Provider', () => {
|
||||
const setMedia = vi.fn();
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia };
|
||||
const value = createContextValue({ setMedia });
|
||||
|
||||
const { result } = renderHook(() => useMediaRegistration(), {
|
||||
const { result } = renderHook(() => useMediaAttach(), {
|
||||
wrapper: createWrapper(value),
|
||||
});
|
||||
|
||||
@@ -63,10 +73,28 @@ describe('useMediaRegistration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useContainerAttach', () => {
|
||||
it('returns undefined outside Provider', () => {
|
||||
const { result } = renderHook(() => useContainerAttach());
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns setContainer inside Provider', () => {
|
||||
const setContainer = vi.fn();
|
||||
const value = createContextValue({ setContainer });
|
||||
|
||||
const { result } = renderHook(() => useContainerAttach(), {
|
||||
wrapper: createWrapper(value),
|
||||
});
|
||||
|
||||
expect(result.current).toBe(setContainer);
|
||||
});
|
||||
});
|
||||
|
||||
describe('usePlayer', () => {
|
||||
it('returns store without selector', () => {
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia: vi.fn() };
|
||||
const value = createContextValue({ store: store as any });
|
||||
|
||||
const { result } = renderHook(() => usePlayer(), {
|
||||
wrapper: createWrapper(value),
|
||||
@@ -96,7 +124,7 @@ describe('useOptionalPlayer', () => {
|
||||
|
||||
it('returns store inside Provider', () => {
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia: vi.fn() };
|
||||
const value = createContextValue({ store: store as any });
|
||||
|
||||
const { result } = renderHook(() => useOptionalPlayer(), {
|
||||
wrapper: createWrapper(value),
|
||||
@@ -107,7 +135,7 @@ describe('useOptionalPlayer', () => {
|
||||
|
||||
it('returns selected state inside Provider', () => {
|
||||
const store = createMockStore({ paused: true });
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia: vi.fn() };
|
||||
const value = createContextValue({ store: store as any });
|
||||
|
||||
const { result } = renderHook(() => useOptionalPlayer((state: any) => state.paused), {
|
||||
wrapper: createWrapper(value),
|
||||
@@ -119,9 +147,8 @@ describe('useOptionalPlayer', () => {
|
||||
|
||||
describe('useMedia', () => {
|
||||
it('returns media from context', () => {
|
||||
const store = createMockStore();
|
||||
const media = document.createElement('video');
|
||||
const value: PlayerContextValue = { store: store as any, media, setMedia: vi.fn() };
|
||||
const value = createContextValue({ media });
|
||||
|
||||
const { result } = renderHook(() => useMedia(), {
|
||||
wrapper: createWrapper(value),
|
||||
@@ -131,8 +158,7 @@ describe('useMedia', () => {
|
||||
});
|
||||
|
||||
it('returns null when no media', () => {
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia: vi.fn() };
|
||||
const value = createContextValue();
|
||||
|
||||
const { result } = renderHook(() => useMedia(), {
|
||||
wrapper: createWrapper(value),
|
||||
@@ -144,8 +170,7 @@ describe('useMedia', () => {
|
||||
|
||||
describe('Container', () => {
|
||||
it('renders children', () => {
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia: vi.fn() };
|
||||
const value = createContextValue();
|
||||
|
||||
const { container } = render(
|
||||
<PlayerContextProvider value={value}>
|
||||
@@ -158,10 +183,9 @@ describe('Container', () => {
|
||||
expect(container.querySelector('span')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('attaches media to store when media is set', () => {
|
||||
const store = createMockStore();
|
||||
const media = document.createElement('video');
|
||||
const value: PlayerContextValue = { store: store as any, media, setMedia: vi.fn() };
|
||||
it('registers container element via setContainer', () => {
|
||||
const setContainer = vi.fn();
|
||||
const value = createContextValue({ setContainer });
|
||||
|
||||
render(
|
||||
<PlayerContextProvider value={value}>
|
||||
@@ -169,15 +193,29 @@ describe('Container', () => {
|
||||
</PlayerContextProvider>
|
||||
);
|
||||
|
||||
expect(store.attach).toHaveBeenCalledWith({
|
||||
media,
|
||||
container: expect.any(HTMLDivElement),
|
||||
});
|
||||
expect(setContainer).toHaveBeenCalledWith(expect.any(HTMLDivElement));
|
||||
});
|
||||
|
||||
it('does not attach when media is null', () => {
|
||||
it('deregisters container on unmount', () => {
|
||||
const setContainer = vi.fn();
|
||||
const value = createContextValue({ setContainer });
|
||||
|
||||
const { unmount } = render(
|
||||
<PlayerContextProvider value={value}>
|
||||
<Container />
|
||||
</PlayerContextProvider>
|
||||
);
|
||||
|
||||
setContainer.mockClear();
|
||||
unmount();
|
||||
|
||||
expect(setContainer).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('does not call store.attach directly', () => {
|
||||
const store = createMockStore();
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia: vi.fn() };
|
||||
const media = document.createElement('video');
|
||||
const value = createContextValue({ store: store as any, media });
|
||||
|
||||
render(
|
||||
<PlayerContextProvider value={value}>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function createMockStore(state: Record<string, unknown> = {}) {
|
||||
*/
|
||||
export function createPlayerWrapper(storeState: Record<string, unknown> = {}) {
|
||||
const store = createMockStore(storeState);
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia: vi.fn() };
|
||||
const value: PlayerContextValue = { store: store as any, media: null, setMedia: vi.fn(), setContainer: vi.fn() };
|
||||
|
||||
return {
|
||||
store,
|
||||
|
||||
Reference in New Issue
Block a user