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);
+ });
+});