Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit da5803d

Browse files
committed
refactor: split useTimeSync into two hooks
1 parent d0a57de commit da5803d

File tree

10 files changed

+138
-102
lines changed

10 files changed

+138
-102
lines changed

site/src/components/SignInLayout/SignInLayout.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { Interpolation, Theme } from "@emotion/react";
2-
import { useTimeSync } from "hooks/useTimeSync";
2+
import { useTimeSyncSelect } from "hooks/useTimeSync";
33
import type { FC, PropsWithChildren } from "react";
44

55
export const SignInLayout: FC<PropsWithChildren> = ({ children }) => {
6-
const year = useTimeSync({
6+
const year = useTimeSyncSelect({
77
targetRefreshInterval: Number.POSITIVE_INFINITY,
8+
selectDependencies: [],
89
select: (date) => date.getFullYear(),
910
});
1011

site/src/hooks/useTimeSync.tsx

+100-71
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ import {
2525
import { useEffectEvent } from "./hookPolyfills";
2626

2727
export {
28-
IDEAL_REFRESH_ONE_DAY,
29-
IDEAL_REFRESH_ONE_HOUR,
30-
IDEAL_REFRESH_ONE_MINUTE,
31-
IDEAL_REFRESH_ONE_SECOND,
28+
TARGET_REFRESH_ONE_DAY,
29+
TARGET_REFRESH_ONE_HOUR,
30+
TARGET_REFRESH_ONE_MINUTE,
31+
TARGET_REFRESH_ONE_SECOND,
3232
} from "utils/TimeSync";
3333

34+
type ReactSubscriptionCallback = (notifyReact: () => void) => () => void;
35+
3436
type SelectCallback = (newSnapshot: Date) => unknown;
3537

3638
type ReactSubscriptionEntry = Readonly<
@@ -47,6 +49,7 @@ type SelectionCacheEntry = Readonly<{ value: unknown }>;
4749

4850
interface ReactTimeSyncApi {
4951
subscribe: (entry: ReactSubscriptionEntry) => () => void;
52+
getTimeSnapshot: () => Date;
5053
getSelectionSnapshot: <T = unknown>(id: string) => T;
5154
invalidateSelection: (id: string, select?: SelectCallback) => void;
5255
}
@@ -63,15 +66,19 @@ class ReactTimeSync implements ReactTimeSyncApi {
6366
// All functions that are part of the public interface must be defined as
6467
// arrow functions, so that they work properly with React
6568

69+
getTimeSnapshot = () => {
70+
return this.#timeSync.getTimeSnapshot();
71+
};
72+
6673
subscribe = (entry: ReactSubscriptionEntry): (() => void) => {
67-
const { select, id, idealRefreshIntervalMs, onUpdate } = entry;
74+
const { select, id, targetRefreshInterval, onUpdate } = entry;
6875

6976
// Make sure that we subscribe first, in case TimeSync is configured to
7077
// invalidate the snapshot on a new subscription. Want to remove risk of
7178
// stale data
7279
const patchedEntry: SubscriptionEntry = {
7380
id,
74-
idealRefreshIntervalMs,
81+
targetRefreshInterval,
7582
onUpdate: (newDate) => {
7683
const prevSelection = this.getSelectionSnapshot(id);
7784
const newSelection = select?.(newDate) ?? newDate;
@@ -157,7 +164,15 @@ export const TimeSyncProvider: FC<TimeSyncProviderProps> = ({
157164
);
158165
};
159166

160-
type UseTimeSyncOptions<T = Date> = Readonly<{
167+
function useTimeSyncContext(): ReactTimeSync {
168+
const timeSync = useContext(timeSyncContext);
169+
if (timeSync === null) {
170+
throw new Error("Cannot call useTimeSync outside of a TimeSyncProvider");
171+
}
172+
return timeSync;
173+
}
174+
175+
type UseTimeSyncOptions = Readonly<{
161176
/**
162177
* targetRefreshInterval is the ideal interval of time, in milliseconds,
163178
* that defines how often the hook should refresh with the newest Date
@@ -172,26 +187,76 @@ type UseTimeSyncOptions<T = Date> = Readonly<{
172187
* sync with other useTimeSync users that are currently mounted on screen.
173188
*/
174189
targetRefreshInterval: number;
190+
}>;
175191

176-
/**
177-
* selectDependencies acts like the dependency array for a useMemo callback.
178-
* Whenever any of the elements in the array change by value, that will
179-
* cause the select callback to re-run synchronously and produce a new,
180-
* up-to-date value for the current render.
181-
*/
182-
selectDependencies?: readonly unknown[];
192+
export function useTimeSync(options: UseTimeSyncOptions): Date {
193+
const { targetRefreshInterval } = options;
194+
const hookId = useId();
195+
const timeSync = useTimeSyncContext();
183196

184-
/**
185-
* Allows you to transform any date values received from the TimeSync class.
186-
* Select functions work similarly to the selects from React Query and Redux
187-
* Toolkit – you don't need to memoize them, and they will only run when the
188-
* underlying TimeSync state has changed.
189-
*
190-
* Select functions must not be async. The hook will error out at the type
191-
* level if you provide one by mistake.
192-
*/
193-
select?: (latestDatetime: Date) => T extends Promise<unknown> ? never : T;
194-
}>;
197+
const subscribe = useCallback<ReactSubscriptionCallback>(
198+
(notifyReact) => {
199+
return timeSync.subscribe({
200+
targetRefreshInterval,
201+
id: hookId,
202+
onUpdate: notifyReact,
203+
});
204+
},
205+
[hookId, timeSync, targetRefreshInterval],
206+
);
207+
208+
const snapshot = useSyncExternalStore(subscribe, timeSync.getTimeSnapshot);
209+
return snapshot;
210+
}
211+
212+
function areDepsInvalidated(
213+
oldDeps: readonly unknown[] | undefined,
214+
newDeps: readonly unknown[] | undefined,
215+
): boolean {
216+
if (oldDeps === undefined) {
217+
if (newDeps === undefined) {
218+
return false;
219+
}
220+
return true;
221+
}
222+
223+
const oldRecast = oldDeps as readonly unknown[];
224+
const newRecast = oldDeps as readonly unknown[];
225+
if (oldRecast.length !== newRecast.length) {
226+
return true;
227+
}
228+
229+
for (const [index, el] of oldRecast.entries()) {
230+
if (el !== newRecast[index]) {
231+
return true;
232+
}
233+
}
234+
235+
return false;
236+
}
237+
238+
type UseTimeSyncSelectOptions<T = Date> = Readonly<
239+
UseTimeSyncOptions & {
240+
/**
241+
* selectDependencies acts like the dependency array for a useMemo
242+
* callback. Whenever any of the elements in the array change by value,
243+
* that will cause the select callback to re-run synchronously and
244+
* produce a new, up-to-date value for the current render.
245+
*/
246+
selectDependencies: readonly unknown[];
247+
248+
/**
249+
* Allows you to transform any date values received from the TimeSync
250+
* class. Select functions work similarly to the selects from React
251+
* Query and Redux Toolkit – you don't need to memoize them, and they
252+
* will only run when the underlying TimeSync state has changed.
253+
*
254+
* Select functions must not be async. The hook will error out at the
255+
* type level if you provide one by mistake.
256+
*/
257+
select: (latestDatetime: Date) => T extends Promise<unknown> ? never : T;
258+
}
259+
>;
195260

196261
/**
197262
* useTimeSync provides React bindings for the TimeSync class, letting a React
@@ -208,21 +273,12 @@ type UseTimeSyncOptions<T = Date> = Readonly<{
208273
* frequent update interval, both component instances will update on that
209274
* interval.
210275
*/
211-
export function useTimeSync<T = Date>(options: UseTimeSyncOptions<T>): T {
212-
const {
213-
select,
214-
selectDependencies: selectDeps,
215-
targetRefreshInterval: idealRefreshIntervalMs,
216-
} = options;
217-
const timeSync = useContext(timeSyncContext);
218-
if (timeSync === null) {
219-
throw new Error("Cannot call useTimeSync outside of a TimeSyncProvider");
220-
}
221-
222-
// Abusing useId a little bit here. It's mainly meant to be used for
223-
// accessibility, but it also gives us a globally unique ID associated with
224-
// whichever component instance is consuming this hook
276+
export function useTimeSyncSelect<T = Date>(
277+
options: UseTimeSyncSelectOptions<T>,
278+
): T {
279+
const { select, selectDependencies, targetRefreshInterval } = options;
225280
const hookId = useId();
281+
const timeSync = useTimeSyncContext();
226282

227283
// This is the one place where we're borderline breaking the React rules.
228284
// useEffectEvent is meant to be used only in useEffect calls, and normally
@@ -249,21 +305,20 @@ export function useTimeSync<T = Date>(options: UseTimeSyncOptions<T>): T {
249305
// the new callback). All other values need to be included in the dependency
250306
// array for correctness, but they should always maintain stable memory
251307
// addresses
252-
type ReactSubscriptionCallback = (notifyReact: () => void) => () => void;
253308
const subscribe = useCallback<ReactSubscriptionCallback>(
254309
(notifyReact) => {
255310
return timeSync.subscribe({
256-
idealRefreshIntervalMs,
311+
targetRefreshInterval: targetRefreshInterval,
257312
id: hookId,
258313
onUpdate: notifyReact,
259314
select: selectForOutsideReact,
260315
});
261316
},
262-
[timeSync, hookId, selectForOutsideReact, idealRefreshIntervalMs],
317+
[timeSync, hookId, selectForOutsideReact, targetRefreshInterval],
263318
);
264319

265-
const [prevDeps, setPrevDeps] = useState(selectDeps);
266-
const depsAreInvalidated = areDepsInvalidated(prevDeps, selectDeps);
320+
const [prevDeps, setPrevDeps] = useState(selectDependencies);
321+
const depsAreInvalidated = areDepsInvalidated(prevDeps, selectDependencies);
267322

268323
const selection = useSyncExternalStore<T>(subscribe, () => {
269324
if (depsAreInvalidated) {
@@ -279,34 +334,8 @@ export function useTimeSync<T = Date>(options: UseTimeSyncOptions<T>): T {
279334
// Setting state mid-render like this is valid, but we just need to make
280335
// sure that we wait until after the useSyncExternalStore state getter runs
281336
if (depsAreInvalidated) {
282-
setPrevDeps(selectDeps);
337+
setPrevDeps(selectDependencies);
283338
}
284339

285340
return selection;
286341
}
287-
288-
function areDepsInvalidated(
289-
oldDeps: readonly unknown[] | undefined,
290-
newDeps: readonly unknown[] | undefined,
291-
): boolean {
292-
if (oldDeps === undefined) {
293-
if (newDeps === undefined) {
294-
return false;
295-
}
296-
return true;
297-
}
298-
299-
const oldRecast = oldDeps as readonly unknown[];
300-
const newRecast = oldDeps as readonly unknown[];
301-
if (oldRecast.length !== newRecast.length) {
302-
return true;
303-
}
304-
305-
for (const [index, el] of oldRecast.entries()) {
306-
if (el !== newRecast[index]) {
307-
return true;
308-
}
309-
}
310-
311-
return false;
312-
}

site/src/modules/workspaces/activity.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ export type WorkspaceActivityStatus =
1010

1111
export function getWorkspaceActivityStatus(
1212
workspace: Workspace,
13+
currentDatetime: Date,
1314
): WorkspaceActivityStatus {
1415
const builtAt = dayjs(workspace.latest_build.created_at);
1516
const usedAt = dayjs(workspace.last_used_at);
16-
const now = dayjs();
17+
const now = dayjs(currentDatetime);
1718

1819
if (workspace.latest_build.status !== "running") {
1920
return "notRunning";

site/src/pages/CliInstallPage/CliInstallPageView.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Interpolation, Theme } from "@emotion/react";
22
import { CodeExample } from "components/CodeExample/CodeExample";
33
import { Welcome } from "components/Welcome/Welcome";
4-
import { useTimeSync } from "hooks/useTimeSync";
4+
import { useTimeSyncSelect } from "hooks/useTimeSync";
55
import type { FC } from "react";
66
import { Link as RouterLink } from "react-router-dom";
77

@@ -10,8 +10,9 @@ type CliInstallPageViewProps = {
1010
};
1111

1212
export const CliInstallPageView: FC<CliInstallPageViewProps> = ({ origin }) => {
13-
const year = useTimeSync({
13+
const year = useTimeSyncSelect({
1414
targetRefreshInterval: Number.POSITIVE_INFINITY,
15+
selectDependencies: [],
1516
select: (date) => date.getFullYear(),
1617
});
1718

site/src/pages/LoginPage/LoginPageView.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Button from "@mui/material/Button";
33
import type { AuthMethods, BuildInfoResponse } from "api/typesGenerated";
44
import { CustomLogo } from "components/CustomLogo/CustomLogo";
55
import { Loader } from "components/Loader/Loader";
6-
import { useTimeSync } from "hooks/useTimeSync";
6+
import { useTimeSyncSelect } from "hooks/useTimeSync";
77
import { type FC, useState } from "react";
88
import { useLocation } from "react-router-dom";
99
import { SignInForm } from "./SignInForm";
@@ -28,8 +28,9 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
2828
onSignIn,
2929
redirectTo,
3030
}) => {
31-
const year = useTimeSync({
31+
const year = useTimeSyncSelect({
3232
targetRefreshInterval: Number.POSITIVE_INFINITY,
33+
selectDependencies: [],
3334
select: (date) => date.getFullYear(),
3435
});
3536
const location = useLocation();

site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
startOfHour,
1818
subDays,
1919
} from "date-fns";
20-
import { IDEAL_REFRESH_ONE_MINUTE, useTimeSync } from "hooks/useTimeSync";
20+
import { TARGET_REFRESH_ONE_MINUTE, useTimeSync } from "hooks/useTimeSync";
2121
import { type ComponentProps, type FC, useRef, useState } from "react";
2222
import { DateRangePicker, createStaticRanges } from "react-date-range";
2323

@@ -44,7 +44,7 @@ interface DateRangeProps {
4444

4545
export const DateRange: FC<DateRangeProps> = ({ value, onChange }) => {
4646
const currentTime = useTimeSync({
47-
targetRefreshInterval: IDEAL_REFRESH_ONE_MINUTE,
47+
targetRefreshInterval: TARGET_REFRESH_ONE_MINUTE,
4848
});
4949
const selectionStatusRef = useRef<"idle" | "selecting">("idle");
5050
const [ranges, setRanges] = useState<RangesState>([

site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
subDays,
4545
} from "date-fns";
4646
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
47-
import { IDEAL_REFRESH_ONE_DAY, useTimeSync } from "hooks/useTimeSync";
47+
import { TARGET_REFRESH_ONE_DAY, useTimeSyncSelect } from "hooks/useTimeSync";
4848
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
4949
import {
5050
type FC,
@@ -70,8 +70,8 @@ export default function TemplateInsightsPage() {
7070
const [searchParams, setSearchParams] = useSearchParams();
7171

7272
const paramsInterval = searchParams.get("interval");
73-
const insightsInterval = useTimeSync<InsightsInterval>({
74-
targetRefreshInterval: IDEAL_REFRESH_ONE_DAY,
73+
const insightsInterval = useTimeSyncSelect<InsightsInterval>({
74+
targetRefreshInterval: TARGET_REFRESH_ONE_DAY,
7575
selectDependencies: [paramsInterval],
7676
select: (newDate) => {
7777
const templateCreateDate = new Date(template.created_at);

site/src/pages/WorkspacePage/AppStatuses.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import type {
1919
} from "api/typesGenerated";
2020
import { useProxy } from "contexts/ProxyContext";
2121
import { formatDistance, formatDistanceToNow } from "date-fns";
22-
import { IDEAL_REFRESH_ONE_MINUTE, useTimeSync } from "hooks/useTimeSync";
22+
import {
23+
TARGET_REFRESH_ONE_MINUTE,
24+
useTimeSyncSelect,
25+
} from "hooks/useTimeSync";
2326
import type { FC } from "react";
2427
import { createAppLinkHref } from "utils/apps";
2528

@@ -153,8 +156,9 @@ export const AppStatuses: FC<AppStatusesProps> = ({
153156
agents,
154157
referenceDate,
155158
}) => {
156-
const comparisonDate = useTimeSync({
157-
targetRefreshInterval: IDEAL_REFRESH_ONE_MINUTE,
159+
const comparisonDate = useTimeSyncSelect({
160+
targetRefreshInterval: TARGET_REFRESH_ONE_MINUTE,
161+
selectDependencies: [referenceDate],
158162
select: (dateState) => referenceDate ?? dateState,
159163
});
160164
const theme = useTheme();

0 commit comments

Comments
 (0)