1

I'm developing a react component that is intended to become an npm package so that it can be imported into various other react projects. There seems to be a problem with using the "useRef" hook.

This is my 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": {}
}

This is my component:

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;

I don't have any problems in bundling or publishing the package on the npm registry, everything goes fine. But when I import my component the application "explodes" at runtime:

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:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. 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.

and

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)

The problem is that if i take this exact code and write it directly into the application that is supposed to import my package, it works perfectly. After some tests I realized that the ref is not working well, like via package it never manages to fill the current param of the ref. But I can't figure out what the issues/differences are between the package and the "pure" code directly in my app.

Can anyone give me some help? Thank you!

UPDATE 1

converting the component from functional component to class purecomponent, reproducing exactly the same behavior, lifecycle and using the callback ref everything works.

So it definitely wasn't a problem of different React versions between package and host etc. Probably handling ref via hooks isn't as stable as in a class component.

This is the new componenet code:

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;

UPDATE 2

Changing the structure and managing the callback ref directly from within my custom hook as @LindaPaiste suggested, now the problem has been solved "in production": if I publish my package in the registry via npm publish, when I download the package from my host application the component works , it keeps giving me the hooks error but only locally now.

NOTICE: To make it works locally too, you have to link (yarn link/npm link) both your package and the peer dependencies of your package in the host application, in my case i had to link just the react package in addition to mine.

Player component code:

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;

Custom hook code:

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 config file:

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": {}
}
  • You’re going to have problems using videoElement as a useEffect dependency because it’s a mutable object, so it won’t trigger a change when .current gets set. Dennis Vash has some good answers on here about that. You may want a callback ref. – Linda Paiste Mar 15 '23 at 16:12
  • @LindaPaiste i updated the answer with new code using callback ref but nothing is changed. surely i'm doing something wrong – Matteo Pietro Peru Mar 15 '23 at 17:01
  • I did not explain myself properly. The advantage of the using a callback function for your ref is that you can execute code directly in the callback function. I’ll write an answer. – Linda Paiste Mar 16 '23 at 15:54
  • The dependency array `[playerState.isPlaying, videoElement]` will behave the same as just `[playerState.isPlaying]` because the `videoElement` object is always the same object. You won't execute those effects when the `.current` is set. But as I'm looking closer that's not an actual problem because A) `videoElement.current` gets set before the `useEffect` is called. B) the initial `playerState` matches the defaults of the video so you aren't needing to call `.pause()` or anything when it's mounted. I rolled back the edit to your question because you did not implement the callback ref properly. – Linda Paiste Mar 16 '23 at 16:15
  • The callback ref is good in situations where you need to execute code when the HTML element gets set, like initializing a charting package on a `div`. It's essentially this: `const videoCallbackRef: React.RefCallback = (element) => { if (element) { /* do stuff that requires the element. */ } }`. – Linda Paiste Mar 16 '23 at 16:31
  • However in your case you *also* need to have access to the HTML element throughout the lifecycle of the component. You can combine the two ref types inside the hook and have the hook return the ref: https://codesandbox.io/s/combined-callback-ref-xt6enr?file=/src/App.tsx But I don't know if that would solve any of your errors, because those seem to be related to the build process and the versioning. – Linda Paiste Mar 16 '23 at 16:32

2 Answers2

1

Most likely this is is issue related to your build process/react version mismatch.

Host app and package should use the same react package. You can try npm ls react and npm ls react-dom. And you should check that you have the one and only instance of react and react-dom package.

If you have different versions you need to fix it. Make sure that react is not bundled inside your library package and that dependencies have compatible versions. You can specify react as peer dependency.

Edit:

Once I've installed your package and checked, you are indeed have react bundled into your code: enter image description here

You need to change your rollup config such that react and react-dom are external

  • I had already added react as to peerDependency, but still added my package.json in the question as well to be safe. This is the result of `npm ls react` in my host application: https://ibb.co/28zNgg0 – Matteo Pietro Peru Mar 15 '23 at 14:53
  • Ok i'll do it! I just wanted to ask you something (that maybe doesn't make sense). So between reporting a package as peerDependency and setting it as external is something that must always be done (if a dependency is "peer" then I set it as external in the rollup bundle config) or is there some deeper logic? thank you – Matteo Pietro Peru Mar 15 '23 at 15:27
  • Btw, change had no effect. Now the bundled file looks like this (seems without React): https://codeshare.io/9OWP64 but i keep getting the same error about hooks. PS: i solved the useRef issue passing it from the host component. It seems to me that now the problem is on my custom hook (useVideoPlayer) where I use hooks inside, like it's not being recognized as a react function – Matteo Pietro Peru Mar 15 '23 at 15:39
  • 1
    Setting package as external mean that it is not included into bundle and will be resolved in the host app. Setting dependency as peer means it should be installed in host app. So it does make sense to pair it, however it is not required to use peer deps, it just might lead to versions mismatch. Also, I checked your code again, and there is no `useRef` used? Is the code in the question correct? – Алексей Мартинкевич Mar 16 '23 at 09:09
  • This morning i published a new version (0.0.5) where i switched from functional to class component that is working, so there's no more hooks in the built code. In that codeshare i linked before there's no "useRef" because i tried to create it from the host and pass it to the package as prop. ps: thanks a lot for these information! – Matteo Pietro Peru Mar 16 '23 at 09:16
1

"Solved"

converting the component from functional component to class purecomponent, reproducing exactly the same behavior, lifecycle and using the callback ref everything works.

So it definitely wasn't a problem of different React versions between package and host etc. Probably handling ref via hooks isn't as stable as in a class component.

This is the new componenet code:

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;
  • You should edit this answer to include the fixed code of the hook and the component. – Linda Paiste Mar 16 '23 at 16:36
  • 1
    @LindaPaiste Thanks for the suggestion, I just edited the answer to include the component code. However I also saw your other answers and so I want to go back to testing using the functional component. – Matteo Pietro Peru Mar 17 '23 at 07:49