Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Window Focus Refetching #247

Closed
verekia opened this issue Jul 28, 2020 · 25 comments
Closed

Window Focus Refetching #247

verekia opened this issue Jul 28, 2020 · 25 comments
Labels
core Feature requests related to core functionality

Comments

@verekia
Copy link

verekia commented Jul 28, 2020

Currently, Apollo Client's pollInterval refetches every n milliseconds, regardless of whether the user is actively using the page or not, which isn't optimized. Rising libraries such as SWR or React Query support focus refetching, meaning that the client will not poll if the user has switched to a different tab or window, and it will immediately fetch again if the user comes back to the tab. This saves a lot of wasted requests, while maintaining a great UX.

It would be nice to see such feature implemented in Apollo Client as well.

This is how these 2 libraries implement the focus:

https://github.com/vercel/swr/search?q=isDocumentVisible&unscoped_q=isDocumentVisible
https://github.com/tannerlinsley/react-query/search?q=isDocumentVisible&unscoped_q=isDocumentVisible

@verekia
Copy link
Author

verekia commented Jul 28, 2020

I made a CodeSandbox showing the basic behavior of the feature.

@xcv58
Copy link

xcv58 commented Oct 28, 2020

+1 for adding this feature. It's very neat and optimized.

@dpkagrawal
Copy link

+1 for this feature as well.

@nklaasse
Copy link

+1

@mgrahamx
Copy link

Would also love to see this feature.

@arvindell
Copy link

This would be a great one!

@Kae7in
Copy link

Kae7in commented Mar 8, 2021

It's a nice to have imo since you can do this quite simply with javascript. There's a good example here: https://spectrum.chat/apollo/apollo-client/how-do-i-refetch-a-query-on-window-focus-with-react-js-next-js~aff68732-35f7-4c04-aa98-e8b66ff096f3

const { refetch, data } = useQuery(...);
useEffect(() => {
  const refetchQuery = () => refetch();
  window.addEventListener('focus', refetchQuery);
  return () => window.removeEventListener('focus', refetchQuery);
});

@verekia
Copy link
Author

verekia commented Mar 8, 2021

@Kae7in It's not just about fetching on refocus. The polling has to stop when the tab is not focused, and the polling sequence should start again on refocus.

@oliverlevay
Copy link

+1 on this feature

2 similar comments
@llc1123
Copy link

llc1123 commented Jan 24, 2022

+1 on this feature

@saritacute
Copy link

+1 on this feature

@arcticfly
Copy link

arcticfly commented Feb 25, 2022

I found success in React with something like this:

const [pollInterval, setPollInterval] = React.useState(5000)
  React.useEffect(() => {
    const startPolling = () => setPollInterval(5000)
    const stopPolling = () => setPollInterval(0)
    window.addEventListener('focus', startPolling)
    window.addEventListener('blur', stopPolling)
    return () => {
      window.removeEventListener('focus', startPolling)
      window.removeEventListener('blur', stopPolling)
    }
  })
  const { loading, error, data } = useQuery(YOUR_GRAPHQL_QUERY, {
    variables: { id },
    pollInterval,
  })

When the user navigates away from the browser tab in which your website is open, the query should stop polling altogether. When the user navigates back, it should once again start.

So far I've only tested on my macBook in Chrome, Safari and Firefox. I don't know what the equivalent of this logic is in every framework that implements Apollo GraphQL, but I imagine there's a similar way to set the poll interval dynamically.

@cmslewis
Copy link

cmslewis commented Mar 9, 2022

Here's an implementation (in TypeScript) that seems to be working for me, though I admittedly haven't tested it thoroughly. It polls initially, stops polling when the window loses focus, then on window re-focus will immediately refetch and start polling again. Further, it aims to provide an API that's as simple as possible to use.

This is adapted from our codebase which uses GraphQL Code Generator, so I'm not sure if this is precisely what the pure Apollo usage would look like.

Usage

const myQueryResult = useQuery(...); // Don't provide pollInterval here.

useQueryPollingWhileWindowFocused({ pollInterval: 10_000, ...myQueryResult });

Implementation

For useQueryPollingWhileWindowFocused

import { useEffect } from "react";

import { useWindowFocus } from "./useWindowFocus";

export namespace useQueryPollingWhileWindowFocused {
  export interface Args {
    pollInterval: number;

    /** The `refetch` function returned from `useQuery`. */
    refetch: () => void;

    /** The `startPolling` function returned from `useQuery`. */
    startPolling: (pollInterval: number) => void;

    /** The `stopPolling` function returned from `useQuery`. */
    stopPolling: () => void;
  }
}

/**
 * Hook that enables polling for a given GraphQL query while the window is focused - and disables
 * polling while the window is not focused. This reduces network traffic to our server while the
 * user isn't literally focused on our application.
 *
 * See the [Apollo docs](https://www.apollographql.com/docs/react/data/queries/#polling) for details
 * about polling.
 */
export function useQueryPollingWhileWindowFocused({
  pollInterval,
  refetch,
  startPolling,
  stopPolling,
}: useQueryPollingWhileWindowFocused.Args): void {
  const { isWindowFocused } = useWindowFocus();

  useEffect(() => {
    if (!isWindowFocused) {
      stopPolling();
    } else {
      // Refetch data immediately when the window is refocused.
      refetch?.();
      startPolling(pollInterval);
    }
  }, [isWindowFocused, pollInterval, refetch, startPolling, stopPolling]);
}

For useWindowFocus

import { useEffect, useState } from "react";

export namespace useWindowFocus {
  export interface Return {
    /** Whether the user's cursor is currently focused in this window. */
    isWindowFocused: boolean;
  }
}

/**
 * Hook that returns whether the window is currently focused. Re-evaluates whenever the window's "is
 * focused" state changes.
 */
// Note: Inspired by https://github.com/jpalumickas/use-window-focus/blob/main/src/index.ts.
export function useWindowFocus(): useWindowFocus.Return {
  const [isWindowFocused, setIsWindowFocused] = useState(hasFocus()); // Focus for first render.

  useEffect(() => {
    setIsWindowFocused(hasFocus()); // Focus for following renders.

    const onFocus = () => setIsWindowFocused(true);
    const onBlur = () => setIsWindowFocused(false);

    window.addEventListener("focus", onFocus);
    window.addEventListener("blur", onBlur);

    return () => {
      window.removeEventListener("focus", onFocus);
      window.removeEventListener("blur", onBlur);
    };
  }, []);

  return { isWindowFocused };
}

function hasFocus() {
  return document.hasFocus();
}

LMK if this does or doesn't work for you.

@adamsoffer
Copy link

adamsoffer commented Mar 16, 2022

+1. Coming from react-query this feels like a deal breaker feature, rather then a nice to have. I unknowingly overspent on bandwidth with my third party data and hosting providers while polling because I assumed apollo only fetched when a tab is active by default. Had to create this messy hack on every query to get around this:

  function usePageVisibility() {
    const [isVisible, setIsVisible] = useState(getIsDocumentVisible());
    const onVisibilityChange = () => setIsVisible(getIsDocumentVisible());
    useEffect(() => {
      const visibilityChange = getBrowserVisibilityProp();
      document.addEventListener(visibilityChange, onVisibilityChange, false);
      return () => {
        document.removeEventListener(visibilityChange, onVisibilityChange);
      };
    });
    return isVisible;
  }
  
  const isVisible = usePageVisibility();
  const {
    data,
    loading,
    startPolling,
    stopPolling,
  } = useQuery(q, {
    variables: {
      account,
    },
    pollInterval: 5000,
  });

  useEffect(() => {
    if (!isVisible) {
      stopPolling();
    } else {
      startPolling(pollInterval);
    }
  }, [isVisible, stopPolling, startPolling]);

A simple refetchOnWindowFocus option on the query ala react-query would go a long way for improving the DX. Is this being prioritized on the roadmap? Thanks!

@leomehr-lumos
Copy link

Big +1 to this feature! This would be extremely helpful for us to refresh app state when a user returns to the window after inactivity.

@LasaleFamine
Copy link

I don't want to be that guy but... this seems that kind of a problem that is going to be forgotten for centuries. Are the workarounds working as you expect guys?

@pranjal-jately-unmind
Copy link

+1 for this feature. Any plans for this to be introduced in the future?

@jpvajda
Copy link
Contributor

jpvajda commented Jul 26, 2022

@LasaleFamine it's not forgotten... 😉 It still has a lot of interest which is great to see.

@jpvajda
Copy link
Contributor

jpvajda commented Jul 27, 2022

👋 If anyone in the community has interest in helping to deliver this feature, we've created an issue to work from our Apollo Client repository.

apollographql/apollo-client#9948

johnnyomair added a commit to vivid-planet/comet that referenced this issue Oct 24, 2022
Apollo does not support pausing polling when the current browser tab is not in focus (see [feature request](apollographql/apollo-feature-requests#247). Therefore we add our own `useFocusAwareQuery` hook that wraps `useQuery` and pauses/resumes polling upon focus/blur.
johnnyomair added a commit to vivid-planet/comet that referenced this issue Oct 24, 2022
Apollo does not support pausing polling when the current browser tab is not in focus (see [feature request](apollographql/apollo-feature-requests#247)). Therefore we add our own `useFocusAwareQuery` hook that wraps `useQuery` and pauses/resumes polling upon focus/blur.
@jerelmiller jerelmiller added the core Feature requests related to core functionality label Apr 6, 2023
@vignesh-kira
Copy link

Is this feature merged by any chance ?

@arvindell
Copy link

arvindell commented Jul 25, 2023

I ended up creating a custom hook that uses the browser's focus event and refetches the query whenever the window gains focus. It has a default 15 second invalidation duration but it can be customized per-query. So far it has worked great for our use case.

Note: this hook also rewrites the loading property if there's a hit in the cache so that the data is not replaced by pending state UI.

import type { OperationVariables, QueryHookOptions, QueryResult, TypedDocumentNode } from '@apollo/client';
import { useQuery } from '@apollo/client';
import type { DocumentNode } from 'graphql';
import { useCallback, useRef, useEffect } from 'react';


const DEFAULT_INVALIDATE_AFTER = 15_000;

/**
 * A wrapper around useQuery that will show stale data while loading. 
 * 
 * @param query - The GraphQL query to run
 * @param options - The query options, forwarded to useQuery
 * @returns The result of the query, with loading set to false even for stale data
 */
export function useData<TData, TVariables extends OperationVariables>(
  query: TypedDocumentNode<TData, TVariables> | DocumentNode,
  options?: QueryHookOptions<TData, TVariables> & {
    invalidateAfter?: number;
  }
): QueryResult<TData, TVariables> {
  const { invalidateAfter = DEFAULT_INVALIDATE_AFTER } = options ?? {};

  const result = useQuery(query, options);
  const lastRefetchAt = useRef(Date.now());

  if (result.loading) {
    const data = result.client.readQuery({
      query: query,
      variables: result.variables,
    });
    if (data) {
      // Rewrite loading to false to show stale but fast data
      result.loading = false;
    }
  }

  // This callback re-fetches the current query if it has not been re-fetched in the last N seconds.
  // We pass it to useOnFocus to re-fetch the query when the app regains focus.
  const onFocus = useCallback(() => {
    const diff = Date.now() - lastRefetchAt.current;
    if (diff > invalidateAfter) {
      lastRefetchAt.current = Date.now();
      result.refetch();
    }
  }, [result, invalidateAfter]);

  useOnFocus({
    onFocus: onFocus,
  });

  return result;
}


function useOnFocus({ onFocus }: { onFocus: () => void }) {
  useEffect(() => {
    const handleFocus = () => {
      onFocus();
    };
    window.addEventListener('focus', handleFocus);
    return () => {
      window.removeEventListener('focus', handleFocus);
    };
  }, [onFocus]);
}

@jerelmiller
Copy link
Member

Hey all 👋 This looks to be a popular request given the number of reactions and responses in this request. Apologies for the lack of response from the Apollo team on this issue.

In an effort to move this forward, I wanted to bring up a point of discussion that I think might be an important distinction for this feature. I see this feature has been referred to as "focus refetching", but looking at various solutions and original issue description, I'd opt to name this something along the lines of "focus polling" instead. Focus refetching, to me, implies that a refetch would occur for a query when the window is focused, not that polling would resume. Focus refetching in this way I think still makes sense for the client in addition to focus polling.

@aditya-kumawat has expressed interest in moving this feature forward so be on the lookout for updates on this feature! Depending on timing, we may try and target this for 3.10.0.

Thanks!

@jerelmiller jerelmiller removed the project-apollo-client (legacy) LEGACY TAG DO NOT USE label Jan 22, 2024
@jerelmiller
Copy link
Member

Hey all 👋 This feature was just released in v3.9.0 with the new skipPollAttempt option (via apollographql/apollo-client#11397).

As such, I'm going to go ahead and close this out as completed as I believe it satisfies the original ask in this request. Big thanks to @aditya-kumawat for the implementation!

@tylerzey
Copy link

Hi @jerelmiller

I feel like this option only satisfies 1/2 of the initial request.

There are two requirements described above:

  1. skip fetch if unfocused
  2. fetch immediately upon focusing

I don't think Apollo natively supports #2 yet outside of a custom implementation by the developer.

For reference: https://tanstack.com/query/v4/docs/framework/react/guides/window-focus-refetching

@jerelmiller
Copy link
Member

jerelmiller commented Jan 30, 2024

@tylerzey that is fair. Reading through the above, many of the suggestions and code solutions were centered around polling, hence why this seemed to satisfy that request, but #2 makes sense on its own outside of polling.

I'd prefer to track it separately though as I feel this thread has grown quite large and might be difficult to parse through (hence why I originally closed this, thinking the request was fully satisfied). Would you be willing to open a new feature request specific to focus refetching? No worries if not. I'd be happy to do it otherwise (it just provides better signal when a non-maintainer opens the request 😆)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Feature requests related to core functionality
Projects
None yet
Development

No branches or pull requests