blogdoodlesgamessounds
blogdoodlesgamessounds
  1. Home
  2. blog
  3. Fullscreen with React

Fullscreen with React

Interact with fullscreen mode in React with `fscreen` using context and hooks.

Jan 11, 2021
#JavaScript#React#fullscreen#hooks#context#fscreen

Motivation

We want to easily allow the user to toggle a fullscreen view of a React component.

Constraints

  • We don't want to care about browser-specific implementations of fullscreen.
  • We want our solution to be agnostic about DOM structure. For example, we don't want to be forced to render a wrapper
    div
    .

Solution

There is already a battle-tested wrapper around the native browser Fullscreen API:

fscreen
(npm, GitHub).

fscreen
exposes a simple interface that normalizes the browser's API:

fscreen.requestFullscreen(domElementToFullscreen);
await fscreen.exitFullscreen();

fscreen
's subscription interface also mirrors the browser's API:

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,

div
et al.

Next, we

useState()
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.

const [active, setActive] = useState(false);

We then subscribe to fullscreen changes using

fscreen
's subscription wrapper.

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

fullscreenRef
element.

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

fullscreenRef
element is indeed currently in fullscreen, as we don't want to force another component to exit fullscreen mode.

const exitFullscreen = useCallback(async () => {
if (fscreen.fullscreenElement === fullscreenRef.current) {
return fscreen.exitFullscreen();
}
}, []);

All together, our

useFullscreen
hook:

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

useMemo()
. 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.

For our final implementation, we wrap our

App
with the
FullscreenProvider
:

<FullscreenProvider>
<App />
</FullscreenProvider>

Now we can call

useFullscreen()
as before, but from any component lower in the hierarchy.