The source code for this blog is available on GitHub.
Note
Top

Basic Usage of useContext in React - video player

Cover Image for Basic Usage of useContext in React - video player
Chen Han
Chen Han

Video Player

Branch Name: HDW-154-HYTE-LP-HAN

img

Similar functionality: Music Player

When developers, new developers in particular, receive this ticket. They might try 2 approaches to manipulate videos if he find it hard to use package and implement it htemselves. This is a conversation with my colleague Daniel, a bestie as well.

"Why it doesn't work if I use videoUrl variable"

I guessed you want to use the packages, but seems thing not went really well as expected. Instead, you wrote it on your own instead of the package.

  • I guess the question is if the image url changes dynamically, y it couldn't change consistently

  • The solution is to use hidden (display: none) property, instead of change variable.

    Videos isn't that reactive. However if you use hidden which means elements are already there, and thus you can deflect most of the edge cases like you mentioned above

  • Secondly you try to operate the video dynamically, and surprise! it didn’t work well with variables

Also ref is confusing entirely

Solution and Feedback

  • I don't think we need to use carousel after doing this
  • Using react context at the top
  • Now ref can props properly
  • Using typescript
  • change absolute bottom-[20.5%] h-[52.7%] to absolute bottom-[20.5%] w-full and put margin auto inside. In that case, you don't need to give height and redefine the position using transformY direction

The first layer component

import { useState, useRef, createContext } from "react";
import VideoPlayer from "@/components/nexus/Carousel/VideoPlayer";
import { carouselBackward, carouselForward } from "@/util/generic";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import TextSlider from "@/components/nexus/Carousel/text-slider";
import classname from "classnames";

type VideoAction = (
  current: number,
  callback?: (id: number) => void
) => {
  el: any;
  id: number;
};

type VideoContextType = {
  currentVideoId: number;
  videos: { url: string, text: string }[];
  videoCarouselForward: VideoAction;
  videoCarouselBackward: VideoAction;
  setCurrentVideoId: (value: number) => void;
};

const videos = [
  {
    url: "https://cdn.sanity.io/files/mqc7p4g4/production/750f7e8f9b3af24f19d3a62c7a06edea60db880a.mp4",
    text: "Custom Lighting"
  },
  {
    url: "https://cdn.sanity.io/files/mqc7p4g4/production/8c9990caf85d5ae794364af6d7bba0ef911081bb.mp4",
    text: "Screen Mirroring"
  },
  {
    url: "https://cdn.sanity.io/files/mqc7p4g4/production/9c073beb0e7384e6f6573976262d8561e407dd5f.mp4",
    text: "Audio Visualization"
  },
];

const initialVideoContextValue = {
  videos,
  currentVideoId: 0,
  setCurrentVideoId: () => {}, // should be there for proper typing
  videoCarouselForward: carouselForward(videos) as unknown as VideoAction,
  videoCarouselBackward: carouselBackward(videos) as unknown as VideoAction,
};

// Create a video context (only apply inside videos)
export const VideoContext = createContext<VideoContextType>(
  initialVideoContextValue
);

export default function NexusVideo() {
  const videosRef = useRef([]);

  const [currentVideoId, setCurrentVideoId] = useState(0);

  const handleVideoStateChange = (id: number) => {
    videos.forEach((video, i) => {
      if (i === id) {
        setCurrentVideoId(id);
      }
    });
  };

  return (
    <VideoContext.Provider
      value={{
        ...initialVideoContextValue,
        currentVideoId,
        setCurrentVideoId,
      }}
    >
      {videos.map((video, index) => (
        <VideoPlayer
          url={video.url}
          index={index}
          ref={videosRef}
          id={currentVideoId}
          key={`video-player-${index}`}
          hidden={videos[currentVideoId] !== video}
          handleVideoStateChange={handleVideoStateChange}
        />
      ))}

      <div className="flex justify-center my-4">
        <TextSlider
          texts={videos.map(video => video.text)}
          slideIndex={currentVideoId}
          className="mx-0 container mt-10 text-white -translate-y-[200px]"
        />
      </div>


      <div className="justify-center w-full flex container text-white text-center -translate-y-[200px]">
        {videos.map((video, index) => (
          <p
            onClick={() => setCurrentVideoId(index)}
            className={classname(
              "mx-1 sm:mx-3 md:mx-4 cursor-pointer",
              index === currentVideoId && "text-primary1 font-bold"
            )}
          >
            {video.text}
          </p>
        ))}
      </div>
    </VideoContext.Provider>
  );
}

The data in the future should come from sanity

const videos = [
  {
    url: "https://cdn.sanity.io/files/mqc7p4g4/production/750f7e8f9b3af24f19d3a62c7a06edea60db880a.mp4",
    text: "Custom Lighting"
  },
  {
    url: "https://cdn.sanity.io/files/mqc7p4g4/production/8c9990caf85d5ae794364af6d7bba0ef911081bb.mp4",
    text: "Screen Mirroring"
  },
  {
    url: "https://cdn.sanity.io/files/mqc7p4g4/production/9c073beb0e7384e6f6573976262d8561e407dd5f.mp4",
    text: "Audio Visualization"
  },
];

and what is carousel backward and forward

const carouselChange = (move: "forward" | "backward") => (array: any[]) => (current: number, callback?: (id: number) => void) => {
  // fetch the current position and mutate at the following step
  let index = current
  
  // forward action
  if (move === "forward") {
    index++
  
    if(current >= array.length - 1) {
      index = 0
    }
  }
  
  // backward action
  if (move === "backward") {
    index--
  
    if(current <= 0) {
      index = array.length - 1
    }
  }
  
  // define your action here
  callback && callback(index)
  
  // return actual element and the current place
  return {
    el: array[index],
    currentId: index
  }
}

export const carouselForward = carouselChange("forward")
export const carouselBackward = carouselChange("backward")

Double currying can be a useful technique in functional programming to create more specialized and reusable functions.

I thought that using the curry function twice would improve readability, but it actually made the code more difficult to understand.

The original version: The reason why I use curry function doubly is to make it more readable, but seems like it turns out unreadable

The revised version:

  1. I thought that using the curry function twice would improve readability, but it actually made the code more difficult to understand.
  2. My intention in using the curry function twice was to enhance readability, but it seems to have had the opposite effect and made the code harder to read.
  3. I believed that employing the curry function doubly would make the code more readable, but it ended up being less readable than before.

Double currying is a technique that allows you to create more specialized versions of a function by partially applying its arguments in stages. In other words, you can create a new function that takes a subset of the original function's arguments, and returns another function that takes the remaining arguments. This can be repeated multiple times until all arguments are supplied, at which point the final result is returned.

Firstly, I am using double currying to create two specialized versions of the carouselChange function, namely carouselForward and carouselBackward. By partially applying the direction argument ("forward" or "backward"), you are creating new functions that only take the items array as an argument.

"Namely" is an adverb that means "that is to say" or "specifically". In the sentence I used, "namely" is used to introduce two specific examples or names of the specialized functions that were created using the double currying technique, which are carouselForward and carouselBackward.

This has several advantages. First, it makes the code more modular and reusable, as you can easily create new versions of carouselChange with different arguments if needed. Second, it makes the code more readable and easier to understand, as the purpose of each specialized function is clearly defined by its name.

export const carouselForward = carouselChange("forward")
export const carouselBackward = carouselChange("backward")

I want to use the carouselForward and carouselBackward functions to create specialized versions of a VideoAction function that you can use in a React context. Here's an example of how I do that:

const videos = []

const initialVideoContextValue = {
  videos,
  currentVideoId: 0,
  setCurrentVideoId: () => {}, // should be there for proper typing
  videoCarouselForward: carouselForward(videos) as unknown as VideoAction,
  videoCarouselBackward: carouselBackward(videos) as unknown as VideoAction,
}

// Create a video context (only apply inside videos)
export const VideoContext = createContext<VideoContextType>(initialVideoContextValue);

We then use this function to create two specialized video carousel actions, videoCarouselForward and videoCarouselBackward, which we add to the context value. Finally, we create the context itself using createContext and the context value, which we can then use in our components with useContext(VideoContext).

Suppose we have an array of numbers that we want to iterate over using these functions. We can create a variable to keep track of the current index in the array, and then call numbersForward and numbersBackward to update the index and print the current number:

const numbers = [1, 32, 55, 78, 30];
let currentIndex = 0;

// Call numbersForward to advance the index and print the current number
numbersForward();
console.log(numbers[currentIndex]); // Output: 32

// Call numbersBackward to rewind the index and print the current number
numbersBackward();
console.log(numbers[currentIndex]); // Output: 1

In this example, we start with currentIndex equal to 0. We then call numbersForward to advance the index, which sets currentIndex to 1. We can then print the current number in the array using numbers[currentIndex], which gives us the second number in the array, 32.

Next, we call numbersBackward to rewind the index, which sets currentIndex back to 0. We can then print the current number again using numbers[currentIndex], which gives us the first number in the array, 1.

By "rewind the index" I simply meant to move the index backwards or towards the beginning of the array. In the example, calling numbersBackward() updates the currentIndex variable to move the index one step towards the beginning of the numbers array. This allows us to "rewind" through the array and access the elements in reverse order.

這使我們能夠透過陣列“倒帶”,並以相反的順序訪問元素。

This shows how the carouselForward and carouselBackward functions can be used to iterate over an array in a flexible way, and how we can use the resulting specialized functions (numbersForward and numbersBackward) to easily manipulate the index and retrieve the current element in the array.

The following is video-player set. Everything pertaining to video are in this component

import Controls from "./Controls";
import { forwardRef, RefObject } from "react";
import classname from "classnames";

interface VideoPlayerProps {
  url: string;
  id: number;
  index: number;
  hidden: boolean;
  
  handleVideoStateChange: (id: number) => void;
}

function VideoPlayer(
  { id, index, url, handleVideoStateChange, hidden }: VideoPlayerProps,
  ref: RefObject<HTMLVideoElement[]>
) {
  return (
    <div className={classname("relative", hidden && "hidden")}>
      <video
        playsInline
        loop
        muted
        preload="true"
        className="object-cover w-[100vw] h-[52.7%]"
        ref={(el) => {
          // @ts-ignore
          ref.current[index] = el;
        }}
      >
        <source src={url} />
      </video>
      <div className="absolute bottom-[20.5%] w-full">
        <Controls
          handleVideoStateChange={handleVideoStateChange}
          // @ts-ignore: inside here is video element
          ref={ref.current[id]}
          id={id}
        />
      </div>
    </div>
  );
}

// @ts-ignore
export default forwardRef(VideoPlayer);

I move forward and backward by onClick action, and inside onClick should be a function. That is the erason why I use double currying

import Play from "@/components/icons/nexus/Play";
import Pause from "@/components/icons/nexus/Pause";
import Forward from "@/components/icons/nexus/Forward";
import Previous from "@/components/icons/nexus/Previous";
import { VideoContext } from "@/components/nexus/nexus-video";
import { forwardRef, useState, useContext } from "react";
import { isEmpty } from "@/util/generic";

interface ControlsProps {
  handleVideoStateChange: (id: number) => void;
  id: number;
}

function Controls(
  { handleVideoStateChange, id }: ControlsProps,
  ref: HTMLVideoElement
) {
  if (isEmpty(ref)) return null;
  
  const videoRef = ref;
  const videoContext = useContext(VideoContext);
  const [isPlaying, setPlay] = useState(!videoRef!.paused);

  const handleVideoPlay = () => {
    if (isPlaying) {
      // video is playing
      videoRef?.pause();
      setPlay(false);
    } else {
      // video is paused or ended
      videoRef?.play();
      setPlay(true);
    }
  };

  const { videoCarouselForward, videoCarouselBackward, setCurrentVideoId } = videoContext;

  return (
    <div className="flex gap-sm justify-center">
      <Previous
        width={40}
        height={40}
        onClick={() => videoCarouselBackward(id, setCurrentVideoId)}
      ></Previous>
      <div onClick={handleVideoPlay}>
        {isPlaying ? (
          <Pause width={40} height={40}></Pause>
        ) : (
          <Play width={40} height={40}></Play>
        )}
      </div>
      <Forward
        width={40}
        height={40}
        onClick={() => videoCarouselForward(id, setCurrentVideoId)}
      ></Forward>
    </div>
  );
}

// @ts-ignore
export default forwardRef(Controls);

My teammate thinks that this code is not very readable. Is there a way to make it more clear?

The phrase "less readability" is grammatically correct, but it might be more natural to say "low readability" or "poor readability." "Less readability" implies a comparison to something else, but without that context, it could be ambiguous or unclear.

The use of currying is definitely a good approach for creating more flexible and reusable code. However, as you mentioned, the implementation can sometimes become difficult to read and understand. Here are some suggestions on how to write the code in a better way:

  1. Consider using default parameters instead of ||:
(current = 0, callback = (id: number) => {}) => {
  // ...
}

This can make the code more concise and easier to read.

  1. Avoid unnecessary variables:
let index = current;
if (move === "forward") {
  index++;
  if (index >= array.length) {
    index = 0;
  }
} else if (move === "backward") {
  index--;
  if (index < 0) {
    index = array.length - 1;
  }
}

In this case, you don't need to create a new index variable, you can just modify the current parameter directly.

  1. Use descriptive function and parameter names:
const getNextElement = (array: any[], currentIndex: number, direction: "forward" | "backward") => {
  let index = currentIndex;
  if (direction === "forward") {
    index++;
    if (index >= array.length) {
      index = 0;
    }
  } else if (direction === "backward") {
    index--;
    if (index < 0) {
      index = array.length - 1;
    }
  }
  return {
    el: array[index],
    currentId: index
  };
}

const carouselForward = (array: any[], current: number, callback: (id: number) => void) => {
  const { el, currentId } = getNextElement(array, current, "forward");
  callback(currentId);
  return el;
}

const carouselBackward = (array: any[], current: number, callback: (id: number) => void) => {
  const { el, currentId } = getNextElement(array, current, "backward");
  callback(currentId);
  return el;
}

By using more descriptive function and parameter names, it can make the code easier to read and understand.

Regarding my question about carousel backward and forward, these are functions that allow you to move backwards or forwards in a carousel or slider of elements. The implementation you provided seems to modify the current index of the element and return the current element and its index after the change.

TextSlider is a text carousel/slider implemented using the react-slick library.

The TextSlider component receives three props:

  • className: a string used to set custom classes to the root div element.
  • slideIndex: a number that represents the index of the initial slide that should be displayed.
  • texts: an array of strings that represent the texts to be displayed in the slider.

Inside the component, a useRef hook is used to create a reference to the Slider component, and an useEffect hook is used to change the current slide index when the slideIndex prop changes.

The settings object is used to configure the Slider component. It defines properties such as the speed of the animation, the number of slides to show at once, and whether the slider should be infinite.

The afterChange and beforeChange properties are two callback functions that are called after or before each slide transition, respectively. In this code, they are logging the current and previous slide indices to the console.

Finally, the component renders a div with a custom class name and the Slider component inside. Each slide in the slider is a div containing a div with the text passed in as a prop.

import Slider from "react-slick";
import classname from "classnames";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import React, { useRef, useEffect } from "react";

const TextSlider = ({
  className,
  slideIndex,
  texts,
}: {
  className: string;
  slideIndex: number;
  texts: string[];
}) => {
  const slider = useRef();

  useEffect(() => {
    // @ts-ignore: move according to the external index
    slider.current.slickGoTo(slideIndex, 2000, false);
  }, [slideIndex]);

  const settings = {
    dots: false,
    arrows: false,
    infinite: true,
    speed: 500,
    slidesToShow: 1,
    slidesToScroll: 1,
    centerMode: true,
    centerPadding: "30%",
    // to monitor or mutate data here
    afterChange: function (index: number) {
      console.log("Slider Changed to:", index);
    },
    beforeChange: function (index: number) {
      console.log("Slider Changed from:", index);
    },
  };

  return (
    <div className={classname(className)}>
      <style jsx global>{`
        div.slick-slider.slick-initialized {
        }

        div.slick-list {
          padding-top: 10px;
          margin: 10px;
        }

        .slick-arrow {
          display: none;
        }

        .slick-active {
          font-weight: bold;
          color: #ffb71b;
        }
      `}</style>
      {/* @ts-ignore */}
      <Slider {...settings} ref={slider}>
        {texts.map((text) => (
          <div className="text-center">
            <div>{text}</div>
          </div>
        ))}
      </Slider>
    </div>
  );
};

export default TextSlider;

TextSlider.defaultProps = {
  className: "",
  slideIndex: 0,
};

TextSlider.defaultProps is an object that sets the default values for any props that are not provided when the component is used. In this case, it sets the default values for className and slideIndex props.

className is a string that specifies the CSS class names to apply to the component. If no className prop is provided when the component is used, the default value of an empty string "" is used.

slideIndex is a number that specifies the index of the slide to be displayed initially. If no slideIndex prop is provided when the component is used, the default value of 0 is used, which displays the first slide in the array of texts.

© 2024 WOOTHINK. All Rights Reserved.
Site MapTerms and ConditionsPrivacy PolicyCookie Policy