Skip to content

Commit

Permalink
Separate client and server concerns in LazyVideo
Browse files Browse the repository at this point in the history
  • Loading branch information
weotch committed Nov 30, 2023
1 parent f92f5ea commit 74ff3e5
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,28 @@
import { useInView } from 'react-intersection-observer'
import { useMediaQueries } from '@react-hook/media-query'
import { useEffect, type ReactElement, useRef, useCallback, type MutableRefObject } from 'react'
import type { LazyVideoProps } from './types/lazyVideoTypes';
import { fillStyles, transparentGif } from './lib/styles'
import type { LazyVideoProps } from '../types/lazyVideoTypes';
import { fillStyles, transparentGif } from '../lib/styles'

type VideoSourceProps = {
src: Required<LazyVideoProps>['src']
videoLoader: LazyVideoProps['videoLoader']
type LazyVideoClientProps = Omit<LazyVideoProps,
'videoLoader' | 'src' | 'sourceMedia'
> & {
srcUrl?: string
mediaSrcs?: Record<string, string>
}

type ResponsiveVideoSourceProps = Pick<Required<LazyVideoProps>,
'src' | 'videoLoader' | 'sourceMedia'
> & {
type ResponsiveVideoSourceProps = {
mediaSrcs: Required<LazyVideoClientProps>['mediaSrcs']
videoRef: VideoRef
}

type VideoRef = MutableRefObject<HTMLVideoElement | undefined>

// An video rendered within a Visual that supports lazy loading
export default function LazyVideo({
src, sourceMedia, videoLoader,
export default function LazyVideoClient({
srcUrl, mediaSrcs,
alt, fit, position, priority, noPoster, paused,
}: LazyVideoProps): ReactElement {
}: LazyVideoClientProps): ReactElement {

// Make a ref to the video so it can be controlled
const videoRef = useRef<HTMLVideoElement>()
Expand Down Expand Up @@ -67,11 +68,6 @@ export default function LazyVideo({
// Simplify logic for whether to load sources
const shouldLoad = priority || inView

// Multiple media queries and a loader func are necessary for responsive
const useResponsiveSource = sourceMedia
&& sourceMedia?.length > 1
&& !!videoLoader

// Render video tag
return (
<video
Expand Down Expand Up @@ -100,39 +96,21 @@ export default function LazyVideo({
}}>

{/* Implement lazy loading by not adding the source until ready */}
{ shouldLoad && (useResponsiveSource ?
<ResponsiveSource { ...{ src, videoLoader, sourceMedia, videoRef }} /> :
<Source {...{ src, videoLoader }} />
{ shouldLoad && (mediaSrcs ?
<ResponsiveSource { ...{ mediaSrcs, videoRef }} /> :
<source src={ srcUrl } type='video/mp4' />
)}
</video>
)
}

// Return a simple source element
function Source({
src, videoLoader
}: VideoSourceProps): ReactElement | undefined {
let srcUrl
if (videoLoader) srcUrl = videoLoader({ src })
else if (typeof src == 'string') srcUrl = src
if (!srcUrl) return
return (<source src={ srcUrl } type='video/mp4' />)
}

// Switch the video asset depending on media queries
function ResponsiveSource({
src, videoLoader, sourceMedia, videoRef
mediaSrcs, videoRef
}: ResponsiveVideoSourceProps): ReactElement | undefined {

// Prepare a hash of source URLs and their media query constraint in the
// style expected by useMediaQueries
const queries = Object.fromEntries(sourceMedia.map(media => {
const url = videoLoader({ src, media })
return [url, media]
}))

// Find the src url that is currently active
const { matches } = useMediaQueries(queries)
const { matches } = useMediaQueries(mediaSrcs)
const srcUrl = getFirstMatch(matches)

// Reload the video since the source changed
Expand Down
55 changes: 55 additions & 0 deletions packages/react/src/LazyVideo/LazyVideoServer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { ReactElement } from 'react'
import type { LazyVideoProps } from '../types/lazyVideoTypes'
import LazyVideoClient from './LazyVideoClient'

// This wrapper function exists to take Function props and make them
// serializable for the LazyVideoClient component, which is a Next.js style
// client component.
export default function LazyVideo(
props: LazyVideoProps
): ReactElement | undefined {

// Destructure some props
const {
src,
sourceMedia,
videoLoader,
} = props

// Multiple media queries and a loader func are necessary for responsive
const useResponsiveSource = sourceMedia
&& sourceMedia?.length > 1
&& !!videoLoader

// Vars that will be conditionally populated
let srcUrl, mediaSrcs

// Prepare a hash of source URLs and their media query constraint in the
// style expected by useMediaQueries.
if (useResponsiveSource) {
const mediaSrcEntries = sourceMedia.map(media => {
const url = videoLoader({ src, media })
return [url, media]
})
// If the array ended up empty, abort
if (mediaSrcEntries.filter(([url]) => !!url).length == 0) return

// Make the hash
mediaSrcs = Object.fromEntries(mediaSrcEntries)

// Make a simple string src url
} else {
if (videoLoader) srcUrl = videoLoader({ src })
else if (typeof src == 'string') srcUrl = src
if (!srcUrl) return // If no url could be built, abort
}

// Render client component
return (
<LazyVideoClient
{...props}
{...{ srcUrl, mediaSrcs }}
/>
)
}

3 changes: 3 additions & 0 deletions packages/react/src/LazyVideo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Export the server component as the defalut LazyVideo component
import LazyVideoServer from './LazyVideoServer'
export default LazyVideoServer
2 changes: 1 addition & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ReactVisual from './ReactVisual'
import LazyVideo from './LazyVideo'
import LazyVideo from './LazyVideo/LazyVideoServer'
import VisualWrapper from './VisualWrapper'
import { collectDataAttributes } from './lib/attributes'

Expand Down

0 comments on commit 74ff3e5

Please sign in to comment.