React Integration
NMMusicPlayer is framework-agnostic.
Wrap it in a custom hook that bridges player events to component state.
Custom hook
// hooks/useMusicPlayer.ts
import { useState, useEffect, useRef } from 'react';
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import {
AutoAdvancePlugin,
MediaSessionPlugin,
} from '@nomercy-entertainment/nomercy-music-player/plugins';
import type { MusicPlaylistItem } from '@nomercy-entertainment/nomercy-music-player';
export function useMusicPlayer() {
const playerRef = useRef(
nmMPlayer('main')
.addPlugin(AutoAdvancePlugin)
.addPlugin(MediaSessionPlugin)
.setup({ playlist: [] }),
);
const [currentTrack, setCurrentTrack] = useState<MusicPlaylistItem | undefined>();
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
useEffect(() => {
const player = playerRef.current;
player.on('current', ({ item }) => setCurrentTrack(item));
player.on('play', () => setIsPlaying(true));
player.on('pause', () => setIsPlaying(false));
player.on('ended', () => setIsPlaying(false));
player.on('time', ({ time }) => setCurrentTime(time));
player.on('duration', ({ duration: dur }) => setDuration(dur));
return () => {
player.dispose();
};
}, []);
return {
player: playerRef.current,
currentTrack,
isPlaying,
currentTime,
duration,
};
}
Using in a component
import { useMusicPlayer } from './hooks/useMusicPlayer';
export function NowPlaying() {
const { player, currentTrack, isPlaying, currentTime, duration } = useMusicPlayer();
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
return (
<div className="player">
<p>{currentTrack?.name ?? 'Nothing playing'}</p>
<p>{currentTrack?.artist}</p>
<div
className="progress"
onClick={(event) => {
const bar = event.currentTarget;
const ratio = event.nativeEvent.offsetX / bar.clientWidth;
void player.time(ratio * player.duration());
}}
>
<div className="fill" style={{ width: `${progress}%` }} />
</div>
<button onClick={() => void player.togglePlayback()}>{isPlaying ? 'Pause' : 'Play'}</button>
<button onClick={() => void player.previous()}>Prev</button>
<button onClick={() => void player.next()}>Next</button>
</div>
);
}
Context pattern
For apps where multiple components need the same player:
import { createContext, useContext, useRef } from 'react';
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import type { IMusicPlayer } from '@nomercy-entertainment/nomercy-music-player';
const PlayerContext = createContext<IMusicPlayer | null>(null);
export function PlayerProvider({ children }: { children: React.ReactNode }) {
const playerRef = useRef(nmMPlayer('global').setup({ playlist: [] }));
return <PlayerContext.Provider value={playerRef.current}>{children}</PlayerContext.Provider>;
}
export function usePlayer(): IMusicPlayer {
const player = useContext(PlayerContext);
if (!player) throw new Error('usePlayer must be used within PlayerProvider');
return player;
}
Error boundary
React class error boundaries catch render errors. Player init errors (bad auth, network failure, codec problems) happen inside the hook, not during render, so you need both layers.
// components/PlayerErrorBoundary.tsx
import { Component, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: (error: Error, retry: () => void) => ReactNode;
}
interface State {
renderError: Error | null;
}
export class PlayerErrorBoundary extends Component<Props, State> {
override state: State = { renderError: null };
static getDerivedStateFromError(error: Error): State {
return { renderError: error };
}
override componentDidCatch(error: Error, info: React.ErrorInfo): void {
console.error('[PlayerErrorBoundary] render error', error, info);
}
retry = (): void => {
this.setState({ renderError: null });
};
override render(): ReactNode {
const { renderError } = this.state;
if (renderError) {
return this.props.fallback
? this.props.fallback(renderError, this.retry)
: (
<div role="alert">
<p>Music player failed to load.</p>
<button onClick={this.retry}>Retry</button>
</div>
);
}
return this.props.children;
}
}
Wire player-level errors from the 'error' and 'fatal' events in your hook, then surface them through React state so the boundary’s sibling can render a recovery UI without unmounting the whole tree:
// hooks/useMusicPlayer.ts (additions to the hook shown above)
import type { PlayerErrorEvent } from '@nomercy-entertainment/nomercy-player-core';
// inside useMusicPlayer():
const [playerError, setPlayerError] = useState<PlayerErrorEvent | null>(null);
// inside the useEffect listener block:
player.on('error', (event) => {
event.markHandled();
setPlayerError(event);
});
player.on('fatal', (event) => {
event.markHandled();
setPlayerError(event);
});
// add `playerError` and `clearPlayerError` to the returned object:
return {
// ...existing fields,
playerError,
clearPlayerError: () => setPlayerError(null),
};
// usage
function PlayerShell() {
const { playerError, clearPlayerError } = useMusicPlayer();
if (playerError) {
return (
<div role="alert">
<p>{playerError.error.message}</p>
{playerError.error.suggestion && <p>{playerError.error.suggestion}</p>}
<button onClick={clearPlayerError}>Dismiss</button>
</div>
);
}
return <NowPlaying />;
}
Performance
Memoize pure display components
Components that read queue position, current track metadata, or volume only need to re-render when those specific values change. Wrap them with React.memo and pass primitive props to keep the comparison cheap:
import { memo } from 'react';
import type { MusicPlaylistItem } from '@nomercy-entertainment/nomercy-music-player';
interface TrackTitleProps {
name: string;
artist: string | undefined;
}
export const TrackTitle = memo(function TrackTitle({ name, artist }: TrackTitleProps) {
return (
<div>
<p>{name}</p>
{artist && <p>{artist}</p>}
</div>
);
});
// In the parent, extract only the primitives the child needs:
const { currentTrack } = useMusicPlayer();
<TrackTitle
name={currentTrack?.name ?? 'Nothing playing'}
artist={currentTrack?.artist}
/>
Debounce high-frequency time events
The 'time' event fires on every timeupdate from the audio backend, which can be several times per second. Do not write raw time state into React on every tick if the component only needs per-second granularity (progress bars, elapsed time labels):
import { useState, useEffect, useRef } from 'react';
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
export function useCurrentTime(intervalMs = 250): number {
const playerRef = useRef(nmMPlayer('main'));
const [displayTime, setDisplayTime] = useState(0);
const pendingRef = useRef<number | undefined>(undefined);
useEffect(() => {
const player = playerRef.current;
player.on('time', ({ time }) => {
if (pendingRef.current !== undefined) return;
pendingRef.current = window.setTimeout(() => {
setDisplayTime(time);
pendingRef.current = undefined;
}, intervalMs);
});
return () => {
if (pendingRef.current !== undefined) {
clearTimeout(pendingRef.current);
}
// do not dispose here — shared player instance
};
}, [intervalMs]);
return displayTime;
}
Note: The
'progress'event (throttled to every 5 seconds by default) is better suited for server-side watch-position saves. Use'time'only when sub-second UI updates are needed.
Clean up listeners to avoid leaks
The player does not auto-remove listeners when a component unmounts. Always return a cleanup function from useEffect. The cleanest pattern is dispose() on the player when the component that owns it unmounts; for shared instances (via context) remove only the listeners you added using the off method:
useEffect(() => {
const player = playerRef.current;
const onPlay = (): void => setIsPlaying(true);
const onPause = (): void => setIsPlaying(false);
player.on('play', onPlay);
player.on('pause', onPause);
return () => {
player.off('play', onPlay);
player.off('pause', onPause);
// call player.dispose() here only if this component OWNS the instance
};
}, []);
Complete multi-component example
This wires the context pattern, error boundary, memoized components, and debounced time into a small but complete app. Tracks use the Sintel and Big Buck Bunny CC films’ audio tracks as real examples.
// App.tsx
import { PlayerProvider } from './context/PlayerContext';
import { PlayerErrorBoundary } from './components/PlayerErrorBoundary';
import { Controls } from './components/Controls';
import { ProgressBar } from './components/ProgressBar';
import { TrackInfo } from './components/TrackInfo';
export default function App() {
return (
<PlayerErrorBoundary>
<PlayerProvider>
<div className="player-shell">
<TrackInfo />
<ProgressBar />
<Controls />
</div>
</PlayerProvider>
</PlayerErrorBoundary>
);
}
// context/PlayerContext.tsx
import { createContext, useContext, useRef, useState, useEffect, type ReactNode } from 'react';
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import {
AutoAdvancePlugin,
MediaSessionPlugin,
} from '@nomercy-entertainment/nomercy-music-player/plugins';
import type { IMusicPlayer, MusicPlaylistItem } from '@nomercy-entertainment/nomercy-music-player';
interface PlayerState {
player: IMusicPlayer;
currentTrack: MusicPlaylistItem | undefined;
isPlaying: boolean;
currentTime: number;
duration: number;
}
const PlayerContext = createContext<PlayerState | null>(null);
const DEMO_PLAYLIST: MusicPlaylistItem[] = [
{
id: 'sintel-score-1',
name: 'Sintel Theme',
artist: 'Jan Morgenstern',
album: 'Sintel OST',
url: 'https://protected.cdn.your-domain.com/audio/sintel-theme.mp3',
duration: 212,
},
{
id: 'bbb-score-1',
name: 'Big Buck Bunny Theme',
artist: 'Jan Morgenstern',
album: 'Big Buck Bunny OST',
url: 'https://protected.cdn.your-domain.com/audio/bbb-theme.mp3',
duration: 596,
},
];
export function PlayerProvider({ children }: { children: ReactNode }) {
const playerRef = useRef(
nmMPlayer('global')
.addPlugin(AutoAdvancePlugin)
.addPlugin(MediaSessionPlugin)
.setup({
baseUrl: 'https://protected.cdn.your-domain.com',
auth: { bearerToken: () => localStorage.getItem('access_token') ?? '' },
playlist: DEMO_PLAYLIST,
}),
);
const [currentTrack, setCurrentTrack] = useState<MusicPlaylistItem | undefined>();
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
useEffect(() => {
const player = playerRef.current;
const onCurrent = ({ item }: { item: MusicPlaylistItem | undefined }): void =>
setCurrentTrack(item);
const onPlay = (): void => setIsPlaying(true);
const onPause = (): void => setIsPlaying(false);
const onEnded = (): void => setIsPlaying(false);
const onTime = ({ time }: { time: number }): void => setCurrentTime(time);
const onDuration = ({ duration: dur }: { duration: number }): void => setDuration(dur);
player.on('current', onCurrent);
player.on('play', onPlay);
player.on('pause', onPause);
player.on('ended', onEnded);
player.on('time', onTime);
player.on('duration', onDuration);
return () => {
player.off('current', onCurrent);
player.off('play', onPlay);
player.off('pause', onPause);
player.off('ended', onEnded);
player.off('time', onTime);
player.off('duration', onDuration);
player.dispose();
};
}, []);
return (
<PlayerContext.Provider value={{ player: playerRef.current, currentTrack, isPlaying, currentTime, duration }}>
{children}
</PlayerContext.Provider>
);
}
export function usePlayer(): PlayerState {
const ctx = useContext(PlayerContext);
if (!ctx) throw new Error('usePlayer must be used within PlayerProvider');
return ctx;
}
// components/TrackInfo.tsx
import { memo } from 'react';
import { usePlayer } from '../context/PlayerContext';
export const TrackInfo = memo(function TrackInfo() {
const { currentTrack } = usePlayer();
return (
<div className="track-info">
<p className="track-name">{currentTrack?.name ?? 'Nothing playing'}</p>
<p className="track-artist">{currentTrack?.artist ?? ''}</p>
</div>
);
});
// components/ProgressBar.tsx
import { usePlayer } from '../context/PlayerContext';
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
export function ProgressBar() {
const { player, currentTime, duration } = usePlayer();
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
return (
<div className="progress-bar">
<span>{formatTime(currentTime)}</span>
<div
className="progress-track"
role="slider"
aria-valuemin={0}
aria-valuemax={duration}
aria-valuenow={currentTime}
aria-label="Seek"
onClick={(event) => {
const bar = event.currentTarget;
const ratio = event.nativeEvent.offsetX / bar.clientWidth;
void player.time(ratio * duration);
}}
>
<div className="progress-fill" style={{ width: `${progress}%` }} />
</div>
<span>{formatTime(duration)}</span>
</div>
);
}
// components/Controls.tsx
import { usePlayer } from '../context/PlayerContext';
export function Controls() {
const { player, isPlaying } = usePlayer();
return (
<div className="controls">
<button aria-label="Previous" onClick={() => void player.previous()}>Prev</button>
<button aria-label={isPlaying ? 'Pause' : 'Play'} onClick={() => void player.togglePlayback()}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<button aria-label="Next" onClick={() => void player.next()}>Next</button>
</div>
);
}
See also
- Quick Start, vanilla TypeScript setup
- Events, all events to bind to state
- Vue Integration, same pattern for Vue 3