apollo-feature-requests icon indicating copy to clipboard operation
apollo-feature-requests copied to clipboard

Window Focus Refetching

Open verekia opened this issue 3 years ago • 19 comments

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 avatar Jul 28 '20 14:07 verekia

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

verekia avatar Jul 28 '20 16:07 verekia

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

xcv58 avatar Oct 28 '20 20:10 xcv58

+1 for this feature as well.

dpkagrawal avatar Oct 28 '20 20:10 dpkagrawal

+1

nklaasse avatar Nov 18 '20 10:11 nklaasse

Would also love to see this feature.

mgrahamx avatar Jan 26 '21 07:01 mgrahamx

This would be a great one!

arvindell avatar Feb 23 '21 01:02 arvindell

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);
});

Kae7in avatar Mar 08 '21 18:03 Kae7in

@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.

verekia avatar Mar 08 '21 18:03 verekia

+1 on this feature

oliverlevay avatar Jan 04 '22 23:01 oliverlevay

+1 on this feature

llc1123 avatar Jan 24 '22 06:01 llc1123

+1 on this feature

saritacute avatar Feb 17 '22 02:02 saritacute

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.

arcticfly avatar Feb 25 '22 17:02 arcticfly

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.

cmslewis avatar Mar 09 '22 21:03 cmslewis

+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!

adamsoffer avatar Mar 16 '22 15:03 adamsoffer

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.

leomehr-lumos avatar Apr 04 '22 21:04 leomehr-lumos

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?

LasaleFamine avatar Apr 24 '22 16:04 LasaleFamine

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

pranjal-jately-unmind avatar May 31 '22 10:05 pranjal-jately-unmind

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

jpvajda avatar Jul 26 '22 23:07 jpvajda

👋 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.

https://github.com/apollographql/apollo-client/issues/9948

jpvajda avatar Jul 27 '22 22:07 jpvajda

Is this feature merged by any chance ?

vignesh-kira avatar Jul 25 '23 16:07 vignesh-kira

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]);
}

arvindell avatar Jul 25 '23 17:07 arvindell

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 avatar Nov 29 '23 21:11 jerelmiller

Hey all 👋 This feature was just released in v3.9.0 with the new skipPollAttempt option (via https://github.com/apollographql/apollo-client/pull/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!

jerelmiller avatar Jan 30 '24 22:01 jerelmiller

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

tylerzey avatar Jan 30 '24 23:01 tylerzey

@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 😆)

jerelmiller avatar Jan 30 '24 23:01 jerelmiller