Basic Usage of useContext in React - video player
Video Player
Branch Name: HDW-154-HYTE-LP-HAN
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%]
toabsolute 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:
- I thought that using the curry function twice would improve readability, but it actually made the code more difficult to understand.
- 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.
- 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
andcarouselBackward
.
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 thecurrentIndex
variable to move the index one step towards the beginning of thenumbers
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:
- Consider using default parameters instead of
||
:
(current = 0, callback = (id: number) => {}) => {
// ...
}
This can make the code more concise and easier to read.
- 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.
- 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 rootdiv
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
.