Skip to content

Commit ca732d6

Browse files
committed
feat: add useYouTube hook
1 parent 145d771 commit ca732d6

File tree

8 files changed

+481
-77
lines changed

8 files changed

+481
-77
lines changed

example/example.js

Lines changed: 0 additions & 46 deletions
This file was deleted.

example/src/index.js

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,61 @@
1-
import React, { useState } from 'react';
1+
import React, { Fragment, useState } from 'react';
22
import ReactDOM from 'react-dom';
3-
import YouTube from 'react-youtube';
3+
import YouTube, { useYouTube } from 'react-youtube';
44

55
import './styles.css';
66

77
const VIDEOS = ['XxVg_s8xAms', '-DX3vJiqxm4'];
88

9+
function YouTubeHookExample() {
10+
const [videoIndex, setVideoIndex] = useState(0);
11+
const [width, setWidth] = useState(600);
12+
const [hidden, setHidden] = useState(false);
13+
const [autoplay, setAutoplay] = useState(false);
14+
15+
const { targetRef, player } = useYouTube({
16+
videoId: VIDEOS[videoIndex],
17+
autoplay,
18+
width,
19+
height: width * (9 / 16),
20+
});
21+
22+
return (
23+
<div className="App">
24+
<div style={{ display: 'flex', marginBottom: '1em' }}>
25+
<button type="button" onClick={() => player.seekTo(120)}>
26+
Seek to 2 minutes
27+
</button>
28+
<button type="button" onClick={() => setVideoIndex((videoIndex + 1) % VIDEOS.length)}>
29+
Change video
30+
</button>
31+
<label>
32+
<input
33+
type="range"
34+
min="300"
35+
max="1080"
36+
value={width}
37+
onChange={(event) => setWidth(event.currentTarget.value)}
38+
/>
39+
Width ({width}px)
40+
</label>
41+
<button type="button" onClick={() => setHidden(!hidden)}>
42+
{hidden ? 'Show' : 'Hide'}
43+
</button>
44+
<label>
45+
<input
46+
type="checkbox"
47+
value={autoplay}
48+
onChange={(event) => setAutoplay(event.currentTarget.checked === false)}
49+
/>
50+
Autoplaying
51+
</label>
52+
</div>
53+
54+
{hidden ? 'mysterious' : <div className="container" ref={targetRef} />}
55+
</div>
56+
);
57+
}
58+
959
function YouTubeComponentExample() {
1060
const [player, setPlayer] = useState(0);
1161
const [videoIndex, setVideoIndex] = useState(0);
@@ -57,4 +107,10 @@ function YouTubeComponentExample() {
57107
);
58108
}
59109

60-
ReactDOM.render(<YouTubeComponentExample />, document.getElementById('app'));
110+
ReactDOM.render(
111+
<Fragment>
112+
<YouTubeComponentExample />
113+
<YouTubeHookExample />
114+
</Fragment>,
115+
document.getElementById('app'),
116+
);

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "7.9.0",
44
"description": "React.js powered YouTube player component",
55
"main": "dist/index.js",
6-
"module": "dist/index.esm.js",
6+
"module": "dist/index.mjs",
77
"types": "index.d.ts",
88
"files": [
99
"dist",
@@ -78,8 +78,8 @@
7878
"scripts": {
7979
"test": "jest",
8080
"test:ci": "jest --ci --runInBand",
81-
"compile:cjs": "babel src/YouTube.js --out-file dist/index.js",
82-
"compile:es": "cross-env BABEL_ENV=es babel src/YouTube.js --out-file dist/index.esm.js",
81+
"compile:cjs": "babel src --out-dir dist",
82+
"compile:es": "cross-env BABEL_ENV=es babel src --out-dir dist --out-file-extension .mjs",
8383
"compile": "npm-run-all --parallel compile:*",
8484
"prepublishOnly": "npm run compile",
8585
"lint": "eslint src example",

src/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import YouTube from './YouTube.js';
2+
import useYouTube from './useYouTube.js';
3+
4+
export default YouTube;
5+
export { useYouTube };
6+
7+
/**
8+
* Expose PlayerState constants for convenience. These constants can also be
9+
* accessed through the global YT object after the YouTube IFrame API is instantiated.
10+
* https://developers.google.com/youtube/iframe_api_reference#onStateChange
11+
*/
12+
export const PlayerState = {
13+
UNSTARTED: -1,
14+
ENDED: 0,
15+
PLAYING: 1,
16+
PAUSED: 2,
17+
BUFFERING: 3,
18+
CUED: 5,
19+
};

src/useYouTube.js

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { useState, useEffect, useRef } from 'react';
2+
3+
function loadScript(url) {
4+
return new Promise((resolve, reject) => {
5+
const script = Object.assign(document.createElement('script'), {
6+
type: 'text/javascript',
7+
charset: 'utf8',
8+
src: url,
9+
async: true,
10+
onerror() {
11+
reject(new Error(`Failed to load: ${url}`));
12+
},
13+
onload() {
14+
resolve();
15+
},
16+
});
17+
18+
document.head.appendChild(script);
19+
});
20+
}
21+
22+
function loadYouTubeIframeApi() {
23+
return new Promise((resolve, reject) => {
24+
const previous = window.onYouTubeIframeAPIReady;
25+
window.onYouTubeIframeAPIReady = () => {
26+
if (previous) previous();
27+
resolve();
28+
};
29+
30+
const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:';
31+
loadScript(`${protocol}//www.youtube.com/iframe_api`).catch(reject);
32+
});
33+
}
34+
35+
function getYouTubeApi() {
36+
if (window.YT && window.YT.Player && window.YT.Player instanceof Function) {
37+
return window.YT;
38+
}
39+
return null;
40+
}
41+
42+
/*
43+
44+
Available options that you can pass to the YouTube Iframe API are
45+
46+
- videoId updated by Player#cueVideoById
47+
48+
- width updated by Player#setSize
49+
- height updated by Player#setSize
50+
51+
- playerVars
52+
- autoplay Player#loadVideoById/Playlist
53+
- cc_lang_pref [static]
54+
- cc_load_policy [static]
55+
- color [static]
56+
- controls [static]
57+
- disablekb [static]
58+
- enablejsapi [static]
59+
- end Player#cue/load* should set endSeconds
60+
- fs [static]
61+
- hl [static]
62+
- iv_load_policy [static]
63+
- list [static]
64+
- listType [static]
65+
- loop updated by Player#setLoop
66+
- modestbranding [static]
67+
- origin [static]
68+
- playlist updated by Player#cue/loadPlaylist
69+
- playsinline [static]
70+
- rel [static]
71+
- start Player#cue/load* should set startSeconds
72+
- widget_referrer [static]
73+
74+
- events [note]
75+
- onReady
76+
- onStateChange
77+
- onPlaybackQualityChange
78+
- onPlaybackRateChange
79+
- onError
80+
- onApiChange
81+
82+
[note] youtube-player fixes the very strange Player#addEventListener behaviour
83+
but does this by overwriting the events property so we can't set these immediately.
84+
85+
*/
86+
87+
/*
88+
89+
type Config = {
90+
videoId: string,
91+
92+
autoplay: boolean,
93+
startSeconds: number,
94+
endSeconds: number,
95+
96+
width: number,
97+
height: number,
98+
99+
onReady: (event: any) => void,
100+
onStateChange: (event: any) => void,
101+
onPlaybackQualityChange: (event: any) => void,
102+
onPlaybackRateChange: (event: any) => void,
103+
onError: (event: any) => void,
104+
onApiChange: (event: any) => void,
105+
};
106+
107+
*/
108+
109+
export default function useYouTube(config, playerVars) {
110+
const [YouTubeApi, setYouTubeApi] = useState(getYouTubeApi);
111+
112+
const [target, setTarget] = useState(null);
113+
const [player, setPlayer] = useState(null);
114+
const configRef = useRef(config);
115+
116+
useEffect(() => {
117+
configRef.current = config;
118+
}, [config]);
119+
120+
useEffect(() => {
121+
if (target === null) return undefined;
122+
123+
const element = target.appendChild(document.createElement('div'));
124+
125+
// TODO: use suspense
126+
if (YouTubeApi === null) {
127+
loadYouTubeIframeApi()
128+
.then(() => setYouTubeApi(getYouTubeApi))
129+
.catch((error) => {
130+
console.error(error);
131+
// TODO: throw so it can be handled by an error boundary
132+
});
133+
return undefined;
134+
}
135+
136+
// NOTE: The YouTube player replaces `element`.
137+
// trying to access it after this point results in unexpected behaviour
138+
const instance = new YouTubeApi.Player(element, {
139+
videoId: configRef.current.videoId,
140+
width: configRef.current.width,
141+
height: configRef.current.height,
142+
playerVars,
143+
events: {
144+
onReady(event) {
145+
setPlayer(instance);
146+
147+
if (typeof configRef.current.onReady === 'function') {
148+
configRef.current.onReady(event);
149+
}
150+
},
151+
onStateChange(event) {
152+
if (typeof configRef.current.onStateChange === 'function') {
153+
configRef.current.onStateChange(event);
154+
}
155+
},
156+
onPlaybackQualityChange(event) {
157+
if (typeof configRef.current.onPlaybackQualityChange === 'function') {
158+
configRef.current.onPlaybackQualityChange(event);
159+
}
160+
},
161+
onPlaybackRateChange(event) {
162+
if (typeof configRef.current.onPlaybackRateChange === 'function') {
163+
configRef.current.onPlaybackRateChange(event);
164+
}
165+
},
166+
onError(event) {
167+
if (typeof configRef.current.onError === 'function') {
168+
configRef.current.onError(event);
169+
}
170+
},
171+
onApiChange(event) {
172+
if (typeof configRef.current.onApiChange === 'function') {
173+
configRef.current.onApiChange(event);
174+
}
175+
},
176+
},
177+
});
178+
179+
return () => {
180+
instance.getIframe().remove();
181+
// TODO: figure out why calling instance.destroy() causes cross origin errors
182+
setPlayer(null);
183+
};
184+
}, [YouTubeApi, target, playerVars]);
185+
186+
// videoId, autoplay, startSeconds, endSeconds
187+
useEffect(() => {
188+
if (player === null) return;
189+
190+
if (!config.videoId) {
191+
player.stopVideo();
192+
return;
193+
}
194+
195+
if (configRef.current.autoplay) {
196+
player.loadVideoById({
197+
videoId: config.videoId,
198+
startSeconds: configRef.current.startSeconds,
199+
endSeconds: configRef.current.endSeconds,
200+
});
201+
return;
202+
}
203+
204+
player.cueVideoById({
205+
videoId: config.videoId,
206+
startSeconds: configRef.current.startSeconds,
207+
endSeconds: configRef.current.endSeconds,
208+
});
209+
}, [player, config.videoId]);
210+
211+
// width, height
212+
useEffect(() => {
213+
if (player === null) return;
214+
215+
if (config.width !== undefined && config.height !== undefined) {
216+
// calling setSize with width and height set to undefined
217+
// makes the player smaller than the default
218+
player.setSize(config.width, config.height);
219+
}
220+
}, [player, config.width, config.height]);
221+
222+
return {
223+
player,
224+
targetRef: setTarget,
225+
};
226+
}

src/Youtube.test.js renamed to tests/Youtube.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import '@testing-library/jest-dom';
22
import React from 'react';
33
import { render, queryByAttribute } from '@testing-library/react';
4-
import YouTube from './YouTube';
4+
import YouTube from '../src/YouTube';
55

66
import Player, { playerMock } from './__mocks__/youtube-player';
77

File renamed without changes.

0 commit comments

Comments
 (0)