When creating a web application you may come across situations where you need to track the current visibility state of the app. You may need to play/pause a video or animation effect, throttle some performance intensive work or simply track the user's behaviour for analytics based on whether the browser tab is active or not. Now, this feature seems pretty simple to implement until you actually try to implement it for the first time. It turns out tracking whether the user is actively interacting with the web application or not can be quite tricky.
There is a Page Visibility API which works fine for most cases but it does not handle all the possible cases of browser tab inactivity. The Page Visibility API that sends a
visibilitychange event to let listeners know that the visible state of the page has changed, has some irregularities. It doesn't fire the event in some of the cases even when the window or concerned browser tab is out of sight or out of focus. To handle some of these edge cases, we need to use a combination of
blur event listeners on both the
document and the
window element. You can find a detailed discussion about it here.
We will implement the workaround logic described in the tutorial mentioned above in a small React app. Don't worry you can read it later, we will explain every aspect of the logic that we will be using.
We will use Codesandbox to bootstrap our React application (you can use create-react-app as well). We will create a small app that will have an HTML5 Video element that will play only when the browser tab is in focus or active otherwise it will be paused automatically. We are using a video because it will make testing our app's functionality easy.
Let's start by creating the simplest piece i.e. the
Video component. It will be a simple component that will receive a Boolean props named
active and a String props named
src that will hold the URL for the video. If the
active props is true then we will play the video otherwise we will pause it.
We will create a simple React class component. We will render a simple video element with its source set to the URL passed using the
src props and use React's new
ref API to attach a
ref on the video DOM node. We will set the video to autoplay assuming when we start the app the page will be active. One thing to note here is Safari now doesn't allow auto-playing media elements without user interaction. The
componentDidUpdate lifecycle method is very handy in handling side effects when a component's props change. Therefore, here we will use this lifecycle method to play and pause the video based on the current value of
Browser vendor prefix differences are very annoying to deal with when using certain APIs and the Page Visibility API is certainly one of them. Therefore, we will create a simple utility function that will handle these differences and return us the compatible API based on the user's browser in a uniform manner. We will create and export this small function from pageVisibilityUtils.js under the src directory.
In this function, we will utilize simple if-else statement based control flow to return the browser-specific API. You can see we attached the ms prefix for Internet Explorer and webkit prefix for Webkit browsers. We will store the correct API in
visibilityChange string variables and return them from the function in the form of a simple object. Lastly, we will export the function.
Next, we move onto our main component. We will encapsulate all of our Page Visibility tracking logic in a reusable React class component by leveraging the Render Props pattern. We will create a class component called
VisibilityManager. This component will handle the adding and removing of all the DOM based event listeners.
We will start by importing the utility function we created earlier and invoking it to get the correct browser specific APIs. Then, we will create the React component and initialize its state with a single field
isVisible set to
true. This Boolean state field will be responsible for reflecting our page visibility state. In the component's
componentDidMount lifecycle method we will attach an event listener on the document for the
visibilitychange event with the
this.handleVisibilityChange method as its handler. We will also attach event listeners for the focus and blur events on the document as well as the window element. This time we will set
this.forceVisibilityFalse as the handlers for the focus and blur events respectively.
Now, we will then create the
handleVisibilityChange method that will accept a single argument
forceFlag argument will be used to determine whether the method is called because of the
visibilitychange event or the focus or blur events. This is so because the
forceVisibilityFalse methods do nothing but call the
handleVisibilityChange method with true and false value for the
forcedFlag argument. Inside the
handleVisibilityChange method, we first check whether the
forcedFlag argument value is a Boolean (this is because if it is called from the
visibilitychange event handler than the argument passed on will be a SyntheticEvent object). If it is a Boolean then we check if it's true or false. When it's true we called the
setVisibility method with true otherwise we call the method with false as an argument. The
setVisibility method leverages
this.setState method to update
isVisible field's value in the component's state. But, if the
forcedFlag is not a Boolean, then we check the hidden attribute value on the document and call the
setVisibility method accordingly. This wraps up our Page Visibility State tracking logic.
To make the component reusable in nature we use the Render Props pattern i.e. instead of rendering a component from the
render method, we invoke
this.props.children as a function with
Lastly, we mount our React app to the DOM in our index.js file. We import our two React components
Video and create a small functional React component
App by composing them. We pass a function as the children of the
VisibilityManager component that accepts
isVisible as an argument and passes it to the
Video component in its return statement. We also pass a video URL as
src props to the
Video component. This is how we consume the Render Props based
VisiblityManager component. Finally, we use
ReactDOM.render method to render our React app on the DOM node with id "root".
Modern browser APIs are getting really powerful and can be used to do amazing things. Most of these APIs are imperative in nature and can be tricky to work with sometimes in React's declarative paradigm. Using powerful patterns like Render Props to wrap these APIs into their own reusable React components can be very useful.