From f792ca2fe18338a2091058a303b7e16b997277ff Mon Sep 17 00:00:00 2001 From: jwbrandon Date: Mon, 10 Aug 2026 15:53:41 -0400 Subject: [PATCH 1/2] docs(readme): document how to reach the player API from a ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The component already exposes the player instance on its ref, but the README only mentioned `this.player`, so consumers had no documented way to get the API and reached for workarounds. Document both routes — ref and didMountCallback — and spell out the two cases where `ref.current.player` is null: before the library resolves, and after unmount. Add tests covering that contract. Closes #31 --- README.md | 52 +++++++++++++++++++++++++++++++++++++ test/jwplayer-react.test.js | 41 +++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/README.md b/README.md index d10da68..4ed362f 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,57 @@ 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 two 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` still sees `null`. +* After the component unmounts. + +Guard every access with `?.`, as above. Event handlers and callbacks run after setup, so they are always safe. + +#### 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 +const [player, setPlayer] = useState(null); + + setPlayer(api)} + willUnmountCallback={() => setPlayer(null)} +/> +``` ## Advanced Implementation Examples diff --git a/test/jwplayer-react.test.js b/test/jwplayer-react.test.js index a78c314..90e3bad 100644 --- a/test/jwplayer-react.test.js +++ b/test/jwplayer-react.test.js @@ -412,3 +412,44 @@ 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', 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(); + }); + + expect(playerRef.current.player).toBe(null); + }); + + 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); + }); +}); From 7c8a5227bc9f394434366f834f1104d5c603c66a Mon Sep 17 00:00:00 2001 From: jwbrandon Date: Mon, 10 Aug 2026 16:05:16 -0400 Subject: [PATCH 2/2] docs(readme): correct the null cases for the player ref Review caught three inaccuracies in the new section. The claim that all callbacks are safe was wrong: componentWillUnmount invokes willUnmountCallback before the player guard, so its player argument can be null, which the shipped types already stated. A failed library load is a third, permanent null case. The useEffect timing claim only holds when the library loads over the network, so soften it to "may". The pending-load test could not fail, because a null window.jwplayer blocks setup no matter what the component does. Assert the transition to a live player instead. Also make the didMountCallback example a complete component so it can be copy-pasted. --- README.md | 33 +++++++++++++++++++++++---------- test/jwplayer-react.test.js | 13 ++++++++++++- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4ed362f..3639c7a 100644 --- a/README.md +++ b/README.md @@ -156,26 +156,39 @@ function Player() { } ``` -`ref.current.player` is `null` in two cases: +`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` still sees `null`. +* 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. Event handlers and callbacks run after setup, so they are always safe. +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 -const [player, setPlayer] = useState(null); +import { useState } from 'react'; +import JWPlayer from '@jwplayer/jwplayer-react'; - setPlayer(api)} - willUnmountCallback={() => setPlayer(null)} -/> +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 90e3bad..1ebce2b 100644 --- a/test/jwplayer-react.test.js +++ b/test/jwplayer-react.test.js @@ -426,7 +426,7 @@ describe('ref access to the player API', () => { expect(playerRef.current.player).toBe(players[playerRef.current.id]); }); - it('leaves player null until the library resolves', async () => { + it('leaves player null until the library resolves, then sets it', async () => { window.jwplayer = null; const playerRef = React.createRef(); @@ -435,8 +435,19 @@ describe('ref access to the player API', () => { 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 () => {