We want to easily allow the user to toggle a fullscreen view of a React component.
.div
There is already a battle-tested wrapper around the native browser Fullscreen API:
(npm, GitHub).fscreen
exposes a simple interface that normalizes the browser's API:fscreen
fscreen.requestFullscreen(domElementToFullscreen);await fscreen.exitFullscreen();
's subscription interface also mirrors the browser's API:fscreen
fscreen.addEventListener('fullscreenchange', handleChange);fscreen.removeEventListener('fullscreenchange', handleChange);
We can thus construct a React hook that taps into this API.
First, we define a React ref using
.useRef()
const fullscreenRef = useRef();
This divorces the hook from any concerns about DOM implementation, as the ref can be later linked with whatever desired element,
et al.div
Next, we
to keep track of the current fullscreen status. This can be useful for modifying styles of components, showing or hiding certain things elsewhere in the app, etc.useState()
const [active, setActive] = useState(false);
We then subscribe to fullscreen changes using
's subscription wrapper.fscreen
useEffect(() => {const handleChange = () => {setActive(fscreen.fullscreenElement === fullscreenRef.current);};fscreen.addEventListener('fullscreenchange', handleChange);return () =>fscreen.removeEventListener('fullscreenchange', handleChange);}, []);
This ensures that our state will be updated when the fullscreen status changes for our
element.fullscreenRef
Finally, we define our callbacks which we can use to enter and exit fullscreen mode. Before entering fullscreen mode, we make sure to exit any existing fullscreen mode (in the event that some other element is already fullscreen).
const enterFullscreen = useCallback(async () => {if (fscreen.fullscreenElement) {await fscreen.exitFullscreen();}return fscreen.requestFullscreen(fullscreenRef.current);}, []);
When we're exiting fullscreen mode, we first confirm that our
element is indeed currently in fullscreen, as we don't want to force another component to exit fullscreen mode.fullscreenRef
const exitFullscreen = useCallback(async () => {if (fscreen.fullscreenElement === fullscreenRef.current) {return fscreen.exitFullscreen();}}, []);
All together, our
hook:useFullscreen
import fscreen from 'fscreen';import { useMemo, useRef, useState, useEffect, useCallback } from 'react';export function useFullscreen() {const fullscreenRef = useRef();const [active, setActive] = useState(false);useEffect(() => {const handleChange = () => {setActive(fscreen.fullscreenElement === fullscreenRef.current);};fscreen.addEventListener('fullscreenchange', handleChange);return () =>fscreen.removeEventListener('fullscreenchange', handleChange);}, []);const enterFullscreen = useCallback(async () => {if (fscreen.fullscreenElement) {await fscreen.exitFullscreen();}return fscreen.requestFullscreen(fullscreenRef.current);}, []);const exitFullscreen = useCallback(async () => {if (fscreen.fullscreenElement === fullscreenRef.current) {return fscreen.exitFullscreen();}}, []);return {fullscreenRef,fullscreenEnabled: fscreen.fullscreenEnabled,fullscreenActive: active,enterFullscreen,exitFullscreen,};}
That's it!
We can now use this in our layout component like so:
function App() {const {fullscreenRef,enterFullscreen,exitFullscreen,fullscreenActive,} = useFullscreen();return (<div>{...headerAndOtherStuff}<main ref={fullscreenRef}>{fullscreenActive ? (<button type="button" onClick={exitFullscreen}>Exit fullscreen mode</button>) : (<button type="button" onClick={enterFullscreen}>Enter fullscreen mode</button>)}{...contentToFullscreen}</main></div>);}
However, it's very likely we'll want to interact with our fullscreen content elsewhere in our app.
We can make that much easier using React's Context API. We'll replace our stand-alone hook with a Context Provider and a hook to use that Context:
import PropTypes from 'prop-types';import fscreen from 'fscreen';import {useMemo,useRef,useState,useEffect,useCallback,createContext,useContext,} from 'react';const FullscreenContext = createContext();export function useFullscreen() {return useContext(FullscreenContext);}export const FullscreenProvider = ({ children }) => {const fullscreenRef = useRef();const [active, setActive] = useState(false);useEffect(() => {const handleChange = () => {setActive(fscreen.fullscreenElement === fullscreenRef.current);};fscreen.addEventListener('fullscreenchange', handleChange);return () =>fscreen.removeEventListener('fullscreenchange', handleChange);}, []);const enterFullscreen = useCallback(async () => {if (fscreen.fullscreenElement) {await fscreen.exitFullscreen();}return fscreen.requestFullscreen(fullscreenRef.current);}, []);const exitFullscreen = useCallback(async () => {if (fscreen.fullscreenElement === fullscreenRef.current) {return fscreen.exitFullscreen();}}, []);const context = useMemo(() => {return {fullscreenRef,fullscreenEnabled: fscreen.fullscreenEnabled,fullscreenActive: active,enterFullscreen,exitFullscreen,};}, [active, enterFullscreen, exitFullscreen]);return (<FullscreenContext.Provider value={context}>{children}</FullscreenContext.Provider>);};FullscreenProvider.propTypes = {children: PropTypes.node,};
Notice here that we added a memoization step using
. This is important in this implementation since we are passing an object as the context value. Context consumers will re-render every time the context value changes, so we need to ensure that the object only changes when necessary. Since prop objects are compared by reference, we memoize the object before passing to the context provider.useMemo()
For our final implementation, we wrap our
with theApp
:FullscreenProvider
<FullscreenProvider><App /></FullscreenProvider>
Now we can call
as before, but from any component lower in the hierarchy.useFullscreen()