Skip to content

Commit

Permalink
reactUtils: Add and use useDateRefreshedAtInterval
Browse files Browse the repository at this point in the history
We'll also start using this in useNotificationReportsByIdentityKey,
to check for a new NotificationProblem that will apply when the
server is about to stop enabling push notifications.
  • Loading branch information
chrisbobbe committed Jan 12, 2024
1 parent 0a7f504 commit 7640668
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 4 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"@octokit/core": "^3.4.0",
"@rollup/plugin-babel": "^5.2.0",
"@rollup/plugin-node-resolve": "^13.0.4",
"@testing-library/react-hooks": "^8.0.1",
"@types/react-native": "~0.67.6",
"@vusion/webfonts-generator": "^0.8.0",
"ast-types": "^0.16.1",
Expand Down
45 changes: 44 additions & 1 deletion src/__tests__/reactUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import React from 'react';
import type { ComponentType } from 'react';
// $FlowFixMe[untyped-import]
import { create, act } from 'react-test-renderer';
// $FlowFixMe[untyped-import]
import * as ReactHooksTesting from '@testing-library/react-hooks';

import { fakeSleep } from './lib/fakeTimers';
import { useHasStayedTrueForMs } from '../reactUtils';
import { useDateRefreshedAtInterval, useHasStayedTrueForMs } from '../reactUtils';

describe('useHasNotChangedForMs', () => {
// This gets indirect coverage because useHasStayedTrueForMs calls it, and
Expand Down Expand Up @@ -33,6 +35,11 @@ describe('useHasStayedTrueForMs', () => {
* repeatedly
* - boring details like how the mock component is implemented
*/
// We wrote these tests a long time before our first experiment with
// @testing-library/react-hooks (for useDateRefreshedAtInterval), in which
// we let the library take care of defining and rendering a component. The
// setup for these older tests is much more verbose.
//
// I'm not totally clear on everything `act` does, but react-test-renderer
// seems to recommend it strongly enough that we actually get errors if we
// don't use it. Following links --
Expand Down Expand Up @@ -249,3 +256,39 @@ describe('useHasStayedTrueForMs', () => {
});
}
});

test('useDateRefreshedAtInterval', async () => {
const interval = 60_000;

function sleep(ms: number): Promise<void> {
return ReactHooksTesting.act(() => fakeSleep(ms));
}

// https://react-hooks-testing-library.com/reference/api#renderhook
const { result } = ReactHooksTesting.renderHook(() => useDateRefreshedAtInterval(interval));

let value = result.current;
expect(result.error).toBeUndefined();

await sleep(interval / 10);
expect(result.error).toBeUndefined();
expect(result.current).toBe(value);

await sleep(interval / 10);
expect(result.error).toBeUndefined();
expect(result.current).toBe(value);

await sleep(interval);
expect(result.error).toBeUndefined();
expect(result.current).not.toBe(value);
expect((result.current - value) * 1000).toBeGreaterThanOrEqual(interval);
value = result.current;

await sleep(interval / 10);
expect(result.error).toBeUndefined();
expect(result.current).toBe(value);

await sleep(interval / 10);
expect(result.error).toBeUndefined();
expect(result.current).toBe(value);
});
7 changes: 4 additions & 3 deletions src/common/ServerPushSetupBanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
notifProblemShortReactText,
} from '../settings/NotifTroubleshootingScreen';
import type { AppNavigationMethods } from '../nav/AppNavigator';
import { useDateRefreshedAtInterval } from '../reactUtils';

type Props = $ReadOnly<{|
navigation: AppNavigationMethods,
Expand All @@ -41,6 +42,8 @@ export default function ServerPushSetupBanner(props: Props): Node {
const silenceServerPushSetupWarnings = useSelector(getSilenceServerPushSetupWarnings);
const realmName = useSelector(getRealmName);

const dateNow = useDateRefreshedAtInterval(60_000);

let visible = false;
let text = '';
if (pushNotificationsEnabled) {
Expand All @@ -49,9 +52,7 @@ export default function ServerPushSetupBanner(props: Props): Node {
// don't show
} else if (
lastDismissedServerPushSetupNotice !== null
// TODO: Could rerender this component on an interval, to give an
// upper bound on how outdated this `new Date()` can be.
&& lastDismissedServerPushSetupNotice >= subWeeks(new Date(), 2)
&& lastDismissedServerPushSetupNotice >= subWeeks(dateNow, 2)
) {
// don't show
} else {
Expand Down
26 changes: 26 additions & 0 deletions src/reactUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,29 @@ export const useHasStayedTrueForMs = (value: boolean, duration: number): boolean
// https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
export const useConditionalEffect = (cb: () => void | (() => void), value: boolean): void =>
React.useEffect(() => (value ? cb() : undefined), [value, cb]);

/**
* A Date, as React state that refreshes regularly at an interval.
*
* Use this in a React component that relies on the current date, to make it
* periodically rerender with a refreshed date value.
*
* Don't change the value of refreshIntervalMs.
*
* This uses `setTimeout`, which is subject to slightly late timeouts:
* https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
* so don't expect millisecond precision.
*/
export const useDateRefreshedAtInterval = (refreshIntervalMs: number): Date => {
useDebugAssertConstant(refreshIntervalMs);

const [date, setDate] = React.useState(new Date());
useConditionalEffect(
React.useCallback(() => {
setDate(new Date());
}, []),
useHasNotChangedForMs(date, refreshIntervalMs),
);

return date;
};
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2595,6 +2595,14 @@
dependencies:
defer-to-connect "^2.0.0"

"@testing-library/react-hooks@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12"
integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-boundary "^3.1.0"

"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
Expand Down Expand Up @@ -10460,6 +10468,13 @@ react-dom@^16.13.1:
prop-types "^15.6.2"
scheduler "^0.19.1"

react-error-boundary@^3.1.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
dependencies:
"@babel/runtime" "^7.12.5"

react-freeze@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d"
Expand Down

0 comments on commit 7640668

Please sign in to comment.