diff --git a/README.md b/README.md index d10da68..3639c7a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ * [Required Props](#required-props) * [Optional Props](#optional-props) * [API Functionality](#api-functionality) + * [With a ref](#with-a-ref) + * [With `didMountCallback`](#with-didmountcallback) * [Advanced Implementation Examples](#advanced-implementation-examples) * [Development](#development) * [Contributing](#contributing) @@ -124,7 +126,70 @@ If you are not using a cloud hosted player you will need to provide a license ke ### API Functionality For advanced usage,`jwplayer-react` creates an instance of the player API when mounted, and sets it to `this.player`, exposing all api functionality listed [here](https://developer.jwplayer.com/jwplayer/docs/jw8-javascript-api-reference). +There are two ways to reach that player instance. +#### With a ref + +`JWPlayer` is a class component, so a ref resolves to the component instance. Read the player API from `ref.current.player`: + +```jsx +import { useCallback, useRef } from 'react'; +import JWPlayer from '@jwplayer/jwplayer-react'; + +function Player() { + const playerRef = useRef(null); + + const mute = useCallback(() => { + playerRef.current?.player?.setMute(true); + }, []); + + return ( + <> + + + + ); +} +``` + +`ref.current.player` is `null` in three cases: + +* Before the player is set up. The component loads the player library over the network, so setup finishes after mount. A parent's first `useEffect` may still see `null`. +* When the library script fails to load. The failure is logged to the console and the player is never created, so `player` stays `null` for the component's whole life. +* After the component unmounts. + +Guard every access with `?.`, as above. `on` and `once` handlers, and `didMountCallback`, only run after setup, so their player is always set. `willUnmountCallback` also fires when setup never happened, so its `player` argument can be `null`. + +#### With `didMountCallback` + +Use `didMountCallback` when you need the player as soon as it exists, for example to store it in state or a context: + +```jsx +import { useState } from 'react'; +import JWPlayer from '@jwplayer/jwplayer-react'; + +function Player() { + const [player, setPlayer] = useState(null); + + return ( + <> + setPlayer(api)} + willUnmountCallback={() => setPlayer(null)} + /> + + + ); +} +``` ## Advanced Implementation Examples diff --git a/test/jwplayer-react.test.js b/test/jwplayer-react.test.js index a78c314..1ebce2b 100644 --- a/test/jwplayer-react.test.js +++ b/test/jwplayer-react.test.js @@ -412,3 +412,55 @@ describe('methods', () => { }); }); }); + +// The ref/player contract documented in the README's "API Functionality" +// section. See https://github.com/jwplayer/jwplayer-react/issues/31 +describe('ref access to the player API', () => { + it('exposes the player API on the ref once setup completes', async () => { + const playerRef = React.createRef(); + + await act(async () => { + render(); + }); + + expect(playerRef.current.player).toBe(players[playerRef.current.id]); + }); + + it('leaves player null until the library resolves, then sets it', async () => { + window.jwplayer = null; + const playerRef = React.createRef(); + + // Render without settling the injected script, matching what a parent's + // first effect sees: mounted component, player not set up yet. + await act(async () => { + render(); + }); + const [script] = Array.from(document.getElementsByTagName('script')) + .filter((tag) => tag.src === library); + + expect(playerRef.current.player).toBe(null); + + // Settling the load must end the null window, otherwise a regression that + // never sets up the player would pass this test too. + window.jwplayer = mockLibrary; + await act(async () => { + script.onload(); + }); + + expect(playerRef.current.player).toBe(players[playerRef.current.id]); + }); + + it('clears player on unmount so late ref reads cannot use a removed player', async () => { + const playerRef = React.createRef(); + let unmount; + + await act(async () => { + ({ unmount } = render()); + }); + const { current: instance } = playerRef; + + unmount(); + + expect(instance.player).toBe(null); + }); +});