React 组件(作为节点包)钩子问题
我正在开发一个 React 组件,旨在将其变成一个 npm 包,以便可以将其导入到其他各种 React 项目中。 使用“useRef”钩子似乎有问题。
这是我的
package.json
:
{
"name": "@mperudirectio/react-player",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/mperudirectio/react-player"
},
"version": "0.0.1",
"author": "matt p",
"license": "MIT",
"scripts": {
"rollup": "rollup -c"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^11.0.0",
"@types/react": "^18.0.28",
"react": "^18.2.0",
"rollup": "^3.19.1",
"rollup-plugin-asset": "^1.1.1",
"rollup-plugin-dts": "^5.2.0",
"rollup-plugin-import-css": "^3.2.1",
"tslib": "^2.5.0",
"typescript": "^4.9.5"
},
"peerDependencies": {
"react": "^18.2.0"
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"files": [
"dist"
],
"types": "dist/index.d.ts",
"dependencies": {}
}
这是我的组件:
import React, { ChangeEvent, FC, useRef, useState, useEffect } from 'react';
import styles from './Player.css' assert { type: 'css' };
document.adoptedStyleSheets = [styles];
// CUSTOM HOOK
const useVideoPlayer = (videoElement: React.MutableRefObject<null | HTMLVideoElement>) => {
const [playerState, setPlayerState] = useState({
isPlaying: false,
progress: 0,
speed: 1,
isMuted: false,
});
const togglePlay = () => {
setPlayerState({
...playerState,
isPlaying: !playerState.isPlaying,
});
};
useEffect(() => {
playerState.isPlaying
? videoElement.current?.play()
: videoElement.current?.pause();
}, [playerState.isPlaying, videoElement]);
const handleOnTimeUpdate = () => {
const progress = ((videoElement.current?.currentTime ?? 1) / (videoElement.current?.duration ?? 1)) * 100;
setPlayerState({
...playerState,
progress,
});
};
const handleVideoProgress = (event: ChangeEvent<HTMLInputElement>) => {
const manualChange = Number(event.target.value);
if (videoElement.current) {
videoElement.current.currentTime = ((videoElement.current?.duration ?? 0) / 100) * manualChange;
setPlayerState({
...playerState,
progress: manualChange,
});
}
};
const handleVideoSpeed = (event: ChangeEvent<HTMLSelectElement>) => {
const speed = Number(event.target.value);
if (videoElement.current) {
videoElement.current.playbackRate = speed;
setPlayerState({
...playerState,
speed,
});
}
};
const toggleMute = () => {
setPlayerState({
...playerState,
isMuted: !playerState.isMuted,
});
};
useEffect(() => {
if (videoElement.current) {
playerState.isMuted
? (videoElement.current.muted = true)
: (videoElement.current.muted = false);
}
}, [playerState.isMuted, videoElement]);
return {
playerState,
togglePlay,
handleOnTimeUpdate,
handleVideoProgress,
handleVideoSpeed,
toggleMute,
};
};
// import video from "./assets/video.mp4";
const v = "https://file-examples.com/storage/fef1706276640fa2f99a5a4/2017/04/file_example_MP4_1280_10MG.mp4";
// MAIN COMPONENT THAT USES CUSTOM HOOK + USEREF
const App: FC = () => {
const videoElement = useRef<null | HTMLVideoElement>(null);
const {
playerState,
togglePlay,
handleOnTimeUpdate,
handleVideoProgress,
handleVideoSpeed,
toggleMute,
} = useVideoPlayer(videoElement);
return (
<div className="container">
<div className="video-wrapper">
<video
src={v}
ref={videoElement}
onTimeUpdate={handleOnTimeUpdate}
/>
<div className="controls">
<div className="actions">
<button onClick={togglePlay}>
{!playerState.isPlaying ? (
<i className="bx bx-play"></i>
) : (
<i className="bx bx-pause"></i>
)}
</button>
</div>
<input
type="range"
min="0"
max="100"
value={playerState.progress}
onChange={(e: ChangeEvent<HTMLInputElement>) => handleVideoProgress(e)}
/>
<select
className="velocity"
value={playerState.speed}
onChange={(e: ChangeEvent<HTMLSelectElement>) => handleVideoSpeed(e)}
>
<option value="0.50">0.50x</option>
<option value="1">1x</option>
<option value="1.25">1.25x</option>
<option value="2">2x</option>
</select>
<button className="mute-btn" onClick={toggleMute}>
{!playerState.isMuted ? (
<i className="bx bxs-volume-full"></i>
) : (
<i className="bx bxs-volume-mute"></i>
)}
</button>
</div>
</div>
</div>
);
};
export default App;
我在 npm 注册表上捆绑或发布包时没有任何问题,一切正常。 但是当我导入我的组件时,应用程序在运行时“爆炸”:
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
- You might have mismatching versions of React and the renderer (such as React DOM)
- You might be breaking the Rules of Hooks
- You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
和
Uncaught TypeError: Cannot read properties of null (reading 'useRef') at Object.useRef (react.development.js:1630:1) at App (Player.tsx:87:1) at renderWithHooks (react-dom.development.js:16305:1) at mountIndeterminateComponent (react-dom.development.js:20074:1) at beginWork (react-dom.development.js:21587:1) at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1) at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1) at invokeGuardedCallback (react-dom.development.js:4277:1) at beginWork$1 (react-dom.development.js:27451:1) at performUnitOfWork (react-dom.development.js:26557:1)
问题是,如果我采用这个确切的代码并将其直接写入应该导入我的包的应用程序中,它就可以完美地工作。经过一些测试后,我意识到 ref 运行得不太好,就像通过包一样,它永远无法填充
ref
的
current
参数。但我无法弄清楚包和应用程序中的“纯”代码之间的问题/差异是什么。
有人能帮我吗?谢谢!
更新 1
将组件从
function component
转换为
class purecomponent
,重现完全相同的行为、生命周期并使用
callback ref
,一切正常。
所以这绝对不是包和主机等之间
React
版本不同的问题。通过
hooks
处理
ref
可能不如在
class component
中那么稳定。
这是新的组件代码:
import React, { ChangeEvent } from 'react';
import styles from './Player.css' assert { type: 'css' };
document.adoptedStyleSheets = [styles];
const v = "https://file-examples.com/storage/fef1706276640fa2f99a5a4/2017/04/file_example_MP4_1280_10MG.mp4";
type PlayerPropz = {
ComponentName?: string
};
type PlayerState = {
isPlaying: boolean,
progress: number,
speed: number,
isMuted: boolean,
};
class Player extends React.PureComponent<PlayerPropz, PlayerState> {
declare private textInput: HTMLVideoElement | null;
declare private setTextInputRef: (element: HTMLVideoElement) => void;
declare public static defaultProps: PlayerPropz;
constructor(props: PlayerPropz) {
super(props);
this.state = {
isPlaying: false,
progress: 0,
speed: 1,
isMuted: false,
}
this.textInput = null;
this.setTextInputRef = (element: HTMLVideoElement) => {
this.textInput = element;
}
this.togglePlay = this.togglePlay.bind(this);
this.handleOnTimeUpdate = this.handleOnTimeUpdate.bind(this);
this.handleVideoProgress = this.handleVideoProgress.bind(this);
this.handleVideoSpeed = this.handleVideoSpeed.bind(this);
this.toggleMute = this.toggleMute.bind(this);
}
componentDidMount(): void {
const { ...state } = this.state;
if (this.textInput) {
state.isPlaying
? this.textInput.play()
: this.textInput.pause();
state.isMuted
? (this.textInput.muted = true)
: (this.textInput.muted = false);
}
}
componentDidUpdate(prevProps: Readonly<PlayerPropz>, prevState: Readonly<PlayerState>, snapshot?: any): void {
const { ...state } = this.state;
if (this.textInput && state.isPlaying != prevState.isPlaying) {
state.isPlaying
? this.textInput.play()
: this.textInput.pause();
}
if (this.textInput && state.isMuted != prevState.isMuted) {
state.isMuted
? (this.textInput.muted = true)
: (this.textInput.muted = false);
}
}
togglePlay() {
this.setState({ isPlaying: !this.state.isPlaying });
}
handleOnTimeUpdate() {
if (this.textInput) {
const progress = (this.textInput.currentTime / this.textInput.duration) * 100;
this.setState({ progress });
}
};
handleVideoProgress(event: ChangeEvent<HTMLInputElement>) {
if (this.textInput) {
const manualChange = Number(event.target.value);
this.textInput.currentTime = (this.textInput.duration / 100) * manualChange;
this.setState({ progress: manualChange });
}
};
handleVideoSpeed(event: ChangeEvent<HTMLSelectElement>) {
const speed = Number(event.target.value);
if (this.textInput) {
this.textInput.playbackRate = speed;
this.setState({ speed });
}
};
toggleMute() {
this.setState({ isMuted: !this.state.isMuted });
};
render() {
const { ...state } = this.state;
return (
<div className="container">
<div className="video-wrapper">
<video
src={v}
ref={(element: HTMLVideoElement) => this.setTextInputRef(element)}
onTimeUpdate={() => this.handleOnTimeUpdate()}
/>
<div className="controls">
<div className="actions">
<button onClick={() => this.togglePlay()}>
{!state.isPlaying ? (
<i className="bx bx-play"></i>
) : (
<i className="bx bx-pause"></i>
)}
</button>
</div>
<input
type="range"
min="0"
max="100"
value={state.progress}
onChange={(e: ChangeEvent<HTMLInputElement>) => this.handleVideoProgress(e)}
/>
<select
className="velocity"
value={state.speed}
onChange={(e: ChangeEvent<HTMLSelectElement>) => this.handleVideoSpeed(e)}
>
<option value="0.50">0.50x</option>
<option value="1">1x</option>
<option value="1.25">1.25x</option>
<option value="2">2x</option>
</select>
<button className="mute-btn" onClick={() => this.toggleMute()}>
{!state.isMuted ? (
<i className="bx bxs-volume-full"></i>
) : (
<i className="bx bxs-volume-mute"></i>
)}
</button>
</div>
</div>
</div>
);
}
};
Player.defaultProps = {
ComponentName: 'Player'
};
export default Player;
更新 2
按照@LindaPaiste 的建议,直接从我的
custom hook
中更改结构并管理
callback ref
,现在问题已经“在生产中”得到解决:如果我通过
npm 在注册表中发布我的包发布
,当我从主机应用程序下载包时,组件可以工作,它一直给我钩子错误,但现在仅限于本地。
注意
:为了使其也在本地工作,您必须
链接
(
yarn link
/
npm link
)主机应用程序中的包和包的对等依赖项,在我的情况下,除了我的包之外,我还必须链接
react
包。
播放器组件代码:
import React, { FC, ChangeEvent } from 'react';
import useVideoPlayer from '../../hooks/useVideoPlayer';
import styles from './Player.css' assert { type: 'css' };
document.adoptedStyleSheets = [styles];
const v = "https://file-examples.com/storage/fef1706276640fa2f99a5a4/2017/04/file_example_MP4_1280_10MG.mp4";
const Player: FC = () => {
const {
playerState,
togglePlay,
handleOnTimeUpdate,
handleVideoProgress,
handleVideoSpeed,
toggleMute,
ref
} = useVideoPlayer();
return (
<div className="container">
<div className="video-wrapper">
<video
src={v}
ref={ref}
onTimeUpdate={handleOnTimeUpdate}
/>
<div className="controls">
<div className="actions">
<button onClick={togglePlay}>
{!playerState.isPlaying ? (
<i className="bx bx-play"></i>
) : (
<i className="bx bx-pause"></i>
)}
</button>
</div>
<input
type="range"
min="0"
max="100"
value={playerState.progress}
onChange={(e: ChangeEvent<HTMLInputElement>) => handleVideoProgress(e)}
/>
<select
className="velocity"
value={playerState.speed}
onChange={(e: ChangeEvent<HTMLSelectElement>) => handleVideoSpeed(e)}
>
<option value="0.50">0.50x</option>
<option value="1">1x</option>
<option value="1.25">1.25x</option>
<option value="2">2x</option>
</select>
<button className="mute-btn" onClick={toggleMute}>
{!playerState.isMuted ? (
<i className="bx bxs-volume-full"></i>
) : (
<i className="bx bxs-volume-mute"></i>
)}
</button>
</div>
</div>
</div>
);
};
export default Player;
自定义钩子代码:
import React, { useState, useRef, useEffect, ChangeEvent } from 'react';
const useVideoPlayer = () => {
const [playerState, setPlayerState] = useState({
isPlaying: false,
progress: 0,
speed: 1,
isMuted: false,
});
const videoElement = useRef<HTMLVideoElement | null>(null);
const videoCallbackRef: React.RefCallback<HTMLVideoElement> = (element: HTMLVideoElement | null) => {
if (element) {
console.log('executed because the HTML video element was set.');
videoElement.current = element;
}
}
useEffect(() => {
if (videoElement.current) {
console.log('executed because playerState.isPlaying changed.');
playerState.isPlaying
? videoElement.current.play()
: videoElement.current.pause();
}
}, [playerState.isPlaying]);
const togglePlay = () => {
setPlayerState({
...playerState,
isPlaying: !playerState.isPlaying,
});
};
const handleOnTimeUpdate = () => {
const progress = ((videoElement.current?.currentTime ?? 1) / (videoElement.current?.duration ?? 1)) * 100;
setPlayerState({
...playerState,
progress,
});
};
const handleVideoProgress = (event: ChangeEvent<HTMLInputElement>) => {
const manualChange = Number(event.target.value);
if (videoElement.current) {
videoElement.current.currentTime = ((videoElement.current?.duration ?? 0) / 100) * manualChange;
setPlayerState({
...playerState,
progress: manualChange,
});
}
};
const handleVideoSpeed = (event: ChangeEvent<HTMLSelectElement>) => {
const speed = Number(event.target.value);
if (videoElement.current) {
videoElement.current.playbackRate = speed;
setPlayerState({
...playerState,
speed,
});
}
};
const toggleMute = () => {
setPlayerState({
...playerState,
isMuted: !playerState.isMuted,
});
};
useEffect(() => {
console.log('executed because playerState.isMuted changed.');
if (videoElement.current) {
playerState.isMuted
? (videoElement.current.muted = true)
: (videoElement.current.muted = false);
}
}, [playerState.isMuted]);
return {
playerState,
togglePlay,
handleOnTimeUpdate,
handleVideoProgress,
handleVideoSpeed,
toggleMute,
ref: videoCallbackRef
};
};
export default useVideoPlayer;
rollup
配置文件:
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import css from 'rollup-plugin-import-css';
import cleanup from 'rollup-plugin-cleanup';
import dts from 'rollup-plugin-dts';
import packageJson from './package.json' assert { type: 'json' };
export default [
{
input: 'src/index.ts',
external: ['react'],
output: [
{
file: packageJson.main,
format: 'cjs',
sourcemap: true,
assetFileNames: "assets/[name]-[hash][extname]"
},
{
file: packageJson.module,
format: 'esm',
sourcemap: true,
assetFileNames: "assets/[name]-[hash][extname]"
},
],
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
css({ modules: true }),
cleanup({ extensions: ['ts', 'tsx', 'js', 'jsx', 'mjs'] })
],
},
{
input: 'dist/esm/types/index.d.ts',
output: [{ file: 'dist/index.d.ts', format: 'esm', sourcemap: true }],
plugins: [dts()],
},
];
package.json
:
{
"name": "@mperudirectio/react-player",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/mperudirectio/react-player"
},
"version": "0.0.6",
"author": "matt p",
"license": "MIT",
"scripts": {
"rollup": "rollup -c"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^11.0.0",
"@types/react": "^18.0.28",
"react": "^18.2.0",
"rollup": "^3.19.1",
"rollup-plugin-asset": "^1.1.1",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-dts": "^5.2.0",
"rollup-plugin-import-css": "^3.2.1",
"tslib": "^2.5.0",
"typescript": "^4.9.5"
},
"peerDependencies": {
"react": "^18.2.0"
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"files": [
"dist"
],
"types": "dist/index.d.ts",
"dependencies": {}
}
这很可能是与您的构建过程/React 版本不匹配有关的问题。
主机应用程序和包应使用相同的 React 包。您可以尝试
npm ls react
和
npm ls react-dom
。并且您应该检查您是否拥有
react
和
react-dom
包的唯一实例。
如果您有不同的版本,则需要修复它。确保 React 未捆绑在您的库包中,并且依赖项具有兼容的版本。您可以将
react
指定为对等依赖项。
编辑:
一旦我安装了您的软件包并进行了检查,您确实已将 react 捆绑到您的代码中:
您需要更改您的汇总配置,以使 react 和 react-dom 为 外部
“已解决”
将组件从
function component
转换为
class purecomponent
,重现完全相同的行为、生命周期并使用
callback ref
一切正常。
因此,这绝对不是包和主机等之间
React
版本不同的问题。通过
hooks
处理
ref
可能不如在
class component
中那么稳定。
这是新的组件代码:
import React, { ChangeEvent } from 'react';
import styles from './Player.css' assert { type: 'css' };
document.adoptedStyleSheets = [styles];
const v = "https://file-examples.com/storage/fef1706276640fa2f99a5a4/2017/04/file_example_MP4_1280_10MG.mp4";
type PlayerPropz = {
ComponentName?: string
};
type PlayerState = {
isPlaying: boolean,
progress: number,
speed: number,
isMuted: boolean,
};
class Player extends React.PureComponent<PlayerPropz, PlayerState> {
declare private textInput: HTMLVideoElement | null;
declare private setTextInputRef: (element: HTMLVideoElement) => void;
declare public static defaultProps: PlayerPropz;
constructor(props: PlayerPropz) {
super(props);
this.state = {
isPlaying: false,
progress: 0,
speed: 1,
isMuted: false,
}
this.textInput = null;
this.setTextInputRef = (element: HTMLVideoElement) => {
this.textInput = element;
}
this.togglePlay = this.togglePlay.bind(this);
this.handleOnTimeUpdate = this.handleOnTimeUpdate.bind(this);
this.handleVideoProgress = this.handleVideoProgress.bind(this);
this.handleVideoSpeed = this.handleVideoSpeed.bind(this);
this.toggleMute = this.toggleMute.bind(this);
}
componentDidMount(): void {
const { ...state } = this.state;
if (this.textInput) {
state.isPlaying
? this.textInput.play()
: this.textInput.pause();
state.isMuted
? (this.textInput.muted = true)
: (this.textInput.muted = false);
}
}
componentDidUpdate(prevProps: Readonly<PlayerPropz>, prevState: Readonly<PlayerState>, snapshot?: any): void {
const { ...state } = this.state;
if (this.textInput && state.isPlaying != prevState.isPlaying) {
state.isPlaying
? this.textInput.play()
: this.textInput.pause();
}
if (this.textInput && state.isMuted != prevState.isMuted) {
state.isMuted
? (this.textInput.muted = true)
: (this.textInput.muted = false);
}
}
togglePlay() {
this.setState({ isPlaying: !this.state.isPlaying });
}
handleOnTimeUpdate() {
if (this.textInput) {
const progress = (this.textInput.currentTime / this.textInput.duration) * 100;
this.setState({ progress });
}
};
handleVideoProgress(event: ChangeEvent<HTMLInputElement>) {
if (this.textInput) {
const manualChange = Number(event.target.value);
this.textInput.currentTime = (this.textInput.duration / 100) * manualChange;
this.setState({ progress: manualChange });
}
};
handleVideoSpeed(event: ChangeEvent<HTMLSelectElement>) {
const speed = Number(event.target.value);
if (this.textInput) {
this.textInput.playbackRate = speed;
this.setState({ speed });
}
};
toggleMute() {
this.setState({ isMuted: !this.state.isMuted });
};
render() {
const { ...state } = this.state;
return (
<div className="container">
<div className="video-wrapper">
<video
src={v}
ref={(element: HTMLVideoElement) => this.setTextInputRef(element)}
onTimeUpdate={() => this.handleOnTimeUpdate()}
/>
<div className="controls">
<div className="actions">
<button onClick={() => this.togglePlay()}>
{!state.isPlaying ? (
<i className="bx bx-play"></i>
) : (
<i className="bx bx-pause"></i>
)}
</button>
</div>
<input
type="range"
min="0"
max="100"
value={state.progress}
onChange={(e: ChangeEvent<HTMLInputElement>) => this.handleVideoProgress(e)}
/>
<select
className="velocity"
value={state.speed}
onChange={(e: ChangeEvent<HTMLSelectElement>) => this.handleVideoSpeed(e)}
>
<option value="0.50">0.50x</option>
<option value="1">1x</option>
<option value="1.25">1.25x</option>
<option value="2">2x</option>
</select>
<button className="mute-btn" onClick={() => this.toggleMute()}>
{!state.isMuted ? (
<i className="bx bxs-volume-full"></i>
) : (
<i className="bx bxs-volume-mute"></i>
)}
</button>
</div>
</div>
</div>
);
}
};
Player.defaultProps = {
ComponentName: 'Player'
};
export default Player;