45

I have a website in ReactJS. I want to get a callback whenever my tab comes in focus or is hidden. I came across the Page Visibility API for this but I'm not able to figure out how to use it in ReactJS.

In which lifecycle method do I register the callback for this?

Pravesh Jain
  • 4,128
  • 6
  • 28
  • 47

8 Answers8

55

Just built this using hooks as of React 16.8

import React, { useEffect } from "react";

// User has switched back to the tab
const onFocus = () => {
    console.log("Tab is in focus");
};

// User has switched away from the tab (AKA tab is hidden)
const onBlur = () => {
    console.log("Tab is blurred");
};

const WindowFocusHandler = () => {
    useEffect(() => {
        window.addEventListener("focus", onFocus);
        window.addEventListener("blur", onBlur);
        // Calls onFocus when the window first loads
        onFocus();
        // Specify how to clean up after this effect:
        return () => {
            window.removeEventListener("focus", onFocus);
            window.removeEventListener("blur", onBlur);
        };
  }, []);

    return <></>;
};

export default WindowFocusHandler;


EDIT May 2023:

There is a new visibilitychange event that has more nuanced behavior for mobile web tabbing. If you need different behavior for when mobile users are viewing their web tabs vs having switched to a different tab you can use that event api as described in more answers below.

Assaf
  • 668
  • 5
  • 5
  • 1
    Its working when I switch tabs, but this is not working if I reload the page and the window is already in focus. I used the exact same code. Is there any reason why this is happening? – Gangula May 14 '21 at 20:23
  • 1
    the listener won't work on initial load. In that case, call `onFocus` one time when the component is mounted. – Asim K T Sep 29 '21 at 12:05
  • I use that solution and in the same try to get the values of `input` in the form and fails. as the states are lost when the tab is not in focus. – Sela Yair May 17 '22 at 12:50
38

This should work:

componentDidMount() {
    window.addEventListener("focus", this.onFocus)
}

componentWillUnmount() {
    window.removeEventListener("focus", this.onFocus)
}

onFocus = () => {
    //
}

Edit: same goes for "blur" and it should work for when the tab becomes hidden.

Check @Assaf's answer for usage with hooks.

Jeffrey Nicholson Carré
  • 2,950
  • 1
  • 26
  • 44
Ali Ankarali
  • 2,761
  • 3
  • 18
  • 30
  • 1
    You must not call function on bind, just bind it like `window.addEventListener("focus", this.onfocus)`. the same is with unbind – Animir Nov 15 '18 at 07:40
  • I am using window.addEventListener('focus', method) but when I click the link alert won't pop-out, but when I go to another tab just to simulate 'leave' and go back again then alert works which is not good. I am using this inside a useEffect hook. Anyone has the idea? – Aljohn Yamaro Mar 22 '20 at 12:33
13

There is no reliable method to check it, so you need to combine few methods together. Here is Context for react-hooks

import React, { useState, useEffect } from 'react'

export const WindowContext = React.createContext(null)

export const WindowContextProvider = props => {
  const [windowIsActive, setWindowIsActive] = useState(true)


  function handleActivity(forcedFlag) {
    if (typeof forcedFlag === 'boolean') {
      return forcedFlag ? setWindowIsActive(true) : setWindowIsActive(false)
    }

    return document.hidden ? setWindowIsActive(false) : setWindowIsActive(true)
  }

  useEffect(() => {
    const handleActivityFalse = () => handleActivity(false)
    const handleActivityTrue = () => handleActivity(true)

    document.addEventListener('visibilitychange', handleActivity)
    document.addEventListener('blur', handleActivityFalse)
    window.addEventListener('blur', handleActivityFalse)
    window.addEventListener('focus', handleActivityTrue )
    document.addEventListener('focus', handleActivityTrue)

    return () => {
      window.removeEventListener('blur', handleActivity)
      document.removeEventListener('blur', handleActivityFalse)
      window.removeEventListener('focus', handleActivityFalse)
      document.removeEventListener('focus', handleActivityTrue )
      document.removeEventListener('visibilitychange', handleActivityTrue )
    }
  }, [])

  return <WindowContext.Provider value={{ windowIsActive }}>{props.children}</WindowContext.Provider>
}
ZiiMakc
  • 31,187
  • 24
  • 65
  • 105
12

I found This library. It could be of help.

Here's how I would use it to solve your problem

import React from 'react';
import PageVisibility from 'react-page-visibility';

class YourComponent extends React.Component {
    state = {
      isWindowInFocus: true,
    }
    componentDidMount() {
      const { isWindowInFocus } = this.props;
      if (!isWindowInFocus) {
        // do something
      }
    }

    listentoWindow = isVisible => {
      this.setState({
        isWindowInFocus: isVisible,
      });
    }
    render() {
      return (
        <PageVisibility onChange={this.listentoWindow}>
          <div>
           Your component JSX
          </div>
        </PageVisibility>
      );
    }
}
Dhia Djobbi
  • 1,176
  • 2
  • 15
  • 35
Allan Guwatudde
  • 533
  • 4
  • 8
6

Modern hook:

import { useCallback, useEffect, useState } from "react";

const useTabActive = () => {
  const [visibilityState, setVisibilityState] = useState(true);

  const handleVisibilityChange = useCallback(() => {
    setVisibilityState(document.visibilityState === 'visible');
  }, []);

  useEffect(() => {
    document.addEventListener("visibilitychange", handleVisibilityChange)
    return () => {
      document.removeEventListener("visibilitychange", handleVisibilityChange)
    }
  }, []);

  return visibilityState;
}

export default useTabActive;

Usage:

const isTabActive = useTabActive();

This hook is utilizing Document.visibilityState

Gal Zakay
  • 286
  • 2
  • 4
1

None of these worked well for what I needed, which was a way to detect if a user switched between tabs, or minimized the browser by double clicking icon in the taskbar.

They either fired off multiple times but did manage registered the correct state, didn't work when minimizing from the taskbar icon or just didn't manage to keep up with multiple actions one after another.

Seeing as I needed to make a server request each time the focus changed, the above situations were a bit 'no'.

So this is what I did:

const DetectChatFocus = () => {
    const [chatFocus, setChatFocus] = useState(true);

    useEffect(() => {
        const handleActivityFalse = () => {
            setChatFocus(false);
            serverRequest(false);
        };

        const handleActivityTrue = () => {
            setChatFocus(true);
            serverRequest(true);
        };

        window.addEventListener('focus', handleActivityTrue);
        window.addEventListener('blur', handleActivityFalse);

        return () => {
            window.removeEventListener('focus', handleActivityTrue);
            window.removeEventListener('blur', handleActivityFalse);
        };
    }, [chatFocus]);
};

export default DetectChatFocus;

Currently this seems to work very well, tested on both Chrome and Firefox, all you need to do is initialize it in a main component or wherever you need and it will keep track of the window focus for all those scenarios and it will only make one server request per action.

Nick09
  • 206
  • 2
  • 15
0

A more complete and optimized hook:

import React, { useState, useEffect } from 'react'
import _ from 'lodash'

export default function useIsWindowFocused(): boolean {
    const [windowIsActive, setWindowIsActive] = useState(true)

    const handleActivity = React.useCallback(
        _.debounce(
            (e: { type: string }) => {
                if (e?.type == 'focus') {
                    return setWindowIsActive(true)
                }
                if (e?.type == 'blur') {
                    return setWindowIsActive(false)
                }
                if (e?.type == 'visibilitychange') {
                    if (document.hidden) {
                        return setWindowIsActive(false)
                    } else {
                        return setWindowIsActive(true)
                    }
                }
            },
            100,
            { leading: false },
        ),
        [],
    )

    useEffect(() => {
        document.addEventListener('visibilitychange', handleActivity)
        document.addEventListener('blur', handleActivity)
        window.addEventListener('blur', handleActivity)
        window.addEventListener('focus', handleActivity)
        document.addEventListener('focus', handleActivity)

        return () => {
            window.removeEventListener('blur', handleActivity)
            document.removeEventListener('blur', handleActivity)
            window.removeEventListener('focus', handleActivity)
            document.removeEventListener('focus', handleActivity)
            document.removeEventListener('visibilitychange', handleActivity)
        }
    }, [])

    return windowIsActive
}
  • Looks interesting, could you explain what debounce does and why you use it? – Albert Schilling Dec 18 '20 at 09:31
  • 1
    Debounce basically prevents a function from running too often. Rather than throttling (regular execution of a function every X ms), debounce holds any number of execution calls until a time limit and then runs the function once. See https://www.geeksforgeeks.org/lodash-_-debounce-method/. I use is to make sure that handleActivity is not triggered more than once every 100ms. It's not a must here but I felt comfortable knowing it was under control. – Samuel Halff Feb 26 '21 at 18:44
0

I think it's simpler to look at the direct state of document.visibilityState, bacause it return 'visible' or 'hidden':

const [visibilityState, setVisibilityState] = useState('visible')

useEffect(() => {
  setVisibilityState(document.visibilityState)
}, [document.visibilityState])
Andronicus
  • 25,419
  • 17
  • 47
  • 88