Skip to content

Commit

Permalink
Merge pull request #856 from vivid-planet/focus-aware-polling
Browse files Browse the repository at this point in the history
Focus aware query polling
  • Loading branch information
kaufmo authored Nov 2, 2022
2 parents 52cfc72 + 47b9cde commit 707d456
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 7 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. This projec

- Add scope support for redirects (`@comet/cms-api` and `@comet/cms-admin`)

### @comet/admin

#### Changes

- Add `useFocusAwarePolling` hook that can be used in combination with `useQuery` to only fetch when the current browser tab is focused

## 3.0.0

_Oct 17, 2022_
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Meta, Story, Canvas } from "@storybook/addon-docs";

<Meta title="Docs/Hooks/useFocusAwarePolling" />

# useFocusAwarePolling()

The `useFocusAwarePolling` hook prevents a `useQuery` hook from polling when the current browser tab is not in focus.
Polling will resume once the tab is back in focus.
Additionally, the query will be updated immediately on focus using `refetch`.

## Basic example

<Canvas>
<Story id="stories-hooks-usefocusawarepolling--basic-example" />
</Canvas>

### Query with `skip` option

When using the `useQuery` hook with the `skip` option enabled (for instance, to pause polling when a dialog is not open), add the `skip` option to `useFocusAwarePolling`.

<Canvas>
<Story id="stories-hooks-usefocusawarepolling--with-skip-option" />
</Canvas>
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { gql, useQuery } from "@apollo/client";
import { useFocusAwarePolling } from "@comet/admin";
import { Pause, Play } from "@comet/admin-icons";
import { Button, Typography } from "@mui/material";
import { storiesOf } from "@storybook/react";
import * as React from "react";

import { apolloStoryDecorator } from "../../../apollo-story.decorator";

storiesOf("stories/hooks/useFocusAwarePolling", module)
.addDecorator(apolloStoryDecorator(`https://api.spacex.land/graphql/`))
.add("Basic example", () => {
const { data, loading, error, refetch, startPolling, stopPolling } = useQuery(
gql`
query LaunchesPast {
launchesPastResult(limit: 1) {
data {
id
mission_name
launch_date_local
}
}
}
`,
{
notifyOnNetworkStatusChange: true, // Only necessary to show loading indicator while polling
},
);

useFocusAwarePolling({
pollInterval: 10000,
refetch,
startPolling,
stopPolling,
});

return (
<>
<Typography>
Most recent launch:
{loading
? "Checking..."
: data && (
<>
{data.launchesPastResult.data[0].mission_name}, {data.launchesPastResult.data[0].launch_date_local}
</>
)}
</Typography>
{error && <Typography color="error">Error: {JSON.stringify(error)}</Typography>}
</>
);
})
.add("With skip option", () => {
const [paused, setPaused] = React.useState(false);

const { data, loading, error, refetch, startPolling, stopPolling } = useQuery(
gql`
query LaunchesPast {
launchesPastResult(limit: 1) {
data {
id
mission_name
launch_date_local
}
}
}
`,
{
notifyOnNetworkStatusChange: true, // Only necessary to show loading indicator while polling
skip: paused,
},
);

useFocusAwarePolling({
pollInterval: 10000,
skip: paused, // <-- Make sure to add skip here too!
refetch,
startPolling,
stopPolling,
});

return (
<>
<Typography>
Most recent launch:
{loading
? "Checking..."
: data && (
<>
{data.launchesPastResult.data[0].mission_name}, {data.launchesPastResult.data[0].launch_date_local}
</>
)}
</Typography>
<Button onClick={() => setPaused(!paused)} startIcon={paused ? <Play /> : <Pause />}>
{paused ? "Resume" : "Pause"}
</Button>
{error && <Typography color="error">Error: {JSON.stringify(error)}</Typography>}
</>
);
});
47 changes: 47 additions & 0 deletions packages/admin/admin/src/apollo/useFocusAwarePolling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ObservableQuery, QueryHookOptions } from "@apollo/client";
import { useEffect } from "react";

type FocusAwarePollingHookOptions = Pick<ObservableQuery, "refetch" | "startPolling" | "stopPolling"> &
Pick<QueryHookOptions, "pollInterval" | "skip">;

function useFocusAwarePolling({ pollInterval, skip, refetch, startPolling, stopPolling }: FocusAwarePollingHookOptions): void {
useEffect(() => {
if (pollInterval === undefined || skip) {
stopPolling();
return;
}

const handleFocus = () => {
refetch();

startPolling(pollInterval);
};

const handleBlur = () => {
stopPolling();
};

window.addEventListener("focus", handleFocus);
window.addEventListener("blur", handleBlur);

let timeoutId: number | undefined;

if (document.hasFocus()) {
// Timeout to prevent duplicate initial requests as useQuery fetches as well
timeoutId = window.setTimeout(() => {
startPolling(pollInterval);
}, pollInterval);
}

return () => {
window.removeEventListener("focus", handleFocus);
window.removeEventListener("blur", handleBlur);

if (timeoutId !== undefined) {
window.clearTimeout(timeoutId);
}
};
}, [pollInterval, skip, refetch, startPolling, stopPolling]);
}

export { useFocusAwarePolling };
1 change: 1 addition & 0 deletions packages/admin/admin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { useFocusAwarePolling } from "./apollo/useFocusAwarePolling";
export { AppHeader, AppHeaderClassKey } from "./appHeader/AppHeader";
export { AppHeaderButton, AppHeaderButtonProps } from "./appHeader/button/AppHeaderButton";
export { AppHeaderButtonClassKey } from "./appHeader/button/AppHeaderButton.styles";
Expand Down
13 changes: 10 additions & 3 deletions packages/admin/cms-admin/src/builds/BuildEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { gql, useQuery } from "@apollo/client";
import { LocalErrorScopeApolloContext } from "@comet/admin";
import { LocalErrorScopeApolloContext, useFocusAwarePolling } from "@comet/admin";
import { SsgRunning, SsgStandby } from "@comet/admin-icons";
import { List, ListItem, Typography } from "@mui/material";
import { styled } from "@mui/material/styles";
Expand Down Expand Up @@ -104,13 +104,20 @@ const BuildStatusPopperContent: React.FunctionComponent<{ data: GQLBuildStatusQu
};

export function BuildEntry(): React.ReactElement {
const { data, error } = useQuery<GQLBuildStatusQuery>(buildStatusQuery, {
pollInterval: process.env.NODE_ENV === "production" ? 10000 : undefined,
const { data, error, refetch, startPolling, stopPolling } = useQuery<GQLBuildStatusQuery>(buildStatusQuery, {
skip: process.env.NODE_ENV === "development",
fetchPolicy: "network-only",
context: LocalErrorScopeApolloContext,
});

useFocusAwarePolling({
pollInterval: process.env.NODE_ENV === "production" ? 10000 : undefined,
skip: process.env.NODE_ENV === "development",
refetch,
startPolling,
stopPolling,
});

const running = data?.builds[0]?.status === "active" || data?.builds[0]?.status === "pending";

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { gql, useQuery } from "@apollo/client";
import { Toolbar, ToolbarActions, ToolbarFillSpace } from "@comet/admin";
import { Toolbar, ToolbarActions, ToolbarFillSpace, useFocusAwarePolling } from "@comet/admin";
import { ArrowRight, Close, Delete } from "@comet/admin-icons";
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, MenuItem, Select } from "@mui/material";
import { styled } from "@mui/material/styles";
Expand Down Expand Up @@ -82,13 +82,20 @@ export default function PageTreeSelectDialog({ value, onChange, open, onClose, d
const classes = useStyles();

// Fetch data
const { data } = useQuery<GQLPagesQuery, GQLPagesQueryVariables>(pagesQuery, {
const { data, refetch, startPolling, stopPolling } = useQuery<GQLPagesQuery, GQLPagesQueryVariables>(pagesQuery, {
variables: {
contentScope: scope,
category,
},
skip: !open,
});

useFocusAwarePolling({
pollInterval: process.env.NODE_ENV === "development" ? undefined : 10000,
skip: !open,
refetch,
startPolling,
stopPolling,
});

// Exclude all archived pages from selectables, except if the selected page itself is archived
Expand Down
21 changes: 19 additions & 2 deletions packages/admin/cms-admin/src/pages/pagesPage/PagesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { useQuery } from "@apollo/client";
import { MainContent, messages, Stack, StackPage, StackSwitch, Toolbar, ToolbarActions, useEditDialog, useStoredState } from "@comet/admin";
import {
MainContent,
messages,
Stack,
StackPage,
StackSwitch,
Toolbar,
ToolbarActions,
useEditDialog,
useFocusAwarePolling,
useStoredState,
} from "@comet/admin";
import { Add } from "@comet/admin-icons";
import { Box, Button, CircularProgress, FormControlLabel, Paper, Switch } from "@mui/material";
import withStyles from "@mui/styles/withStyles";
Expand Down Expand Up @@ -53,12 +64,18 @@ export function PagesPage({
};
}, [setRedirectPathAfterChange, path]);

const { loading, data } = useQuery<GQLPagesQuery, GQLPagesQueryVariables>(pagesQuery, {
const { loading, data, refetch, startPolling, stopPolling } = useQuery<GQLPagesQuery, GQLPagesQueryVariables>(pagesQuery, {
variables: {
contentScope: scope,
category,
},
});

useFocusAwarePolling({
pollInterval: process.env.NODE_ENV === "development" ? undefined : 10000,
refetch,
startPolling,
stopPolling,
});

const [EditDialog, editDialogSelection, editDialogApi] = useEditDialog();
Expand Down

0 comments on commit 707d456

Please sign in to comment.