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

Skip to content

Commit f352e3b

Browse files
committed
fix: remove dependencies from hook API
1 parent 5ba068a commit f352e3b

File tree

7 files changed

+64
-65
lines changed

7 files changed

+64
-65
lines changed

site/src/components/SignInLayout/SignInLayout.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { FC, PropsWithChildren } from "react";
55
export const SignInLayout: FC<PropsWithChildren> = ({ children }) => {
66
const year = useTimeSyncSelect({
77
targetRefreshInterval: Number.POSITIVE_INFINITY,
8-
selectDependencies: [],
98
select: (date) => date.getFullYear(),
109
});
1110

site/src/hooks/useTimeSync.tsx

+63-58
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,56 @@ class ReactTimeSync implements ReactTimeSyncApi {
6868
this.#selectionCache = new Map();
6969
}
7070

71+
#areValuesDeepEqual(value1: unknown, value2: unknown): boolean {
72+
// JavaScript is fun and doesn't have a 100% foolproof comparison
73+
// operation. Object.is covers the most cases, but you still need to
74+
// compare 0 values, because even though JS programmers almost never
75+
// care about +0 vs -0, Object.is does treat them as not being equal
76+
if (Object.is(value1, value2)) {
77+
return true;
78+
}
79+
if (value1 === 0 && value2 === 0) {
80+
return true;
81+
}
82+
83+
if (value1 instanceof Date && value2 instanceof Date) {
84+
return value1.getMilliseconds() === value2.getMilliseconds();
85+
}
86+
87+
// Can't reliably compare functions; just have to treat them as always
88+
// different. Hopefully no one is storing functions in state for this
89+
// hook, though
90+
if (typeof value1 === "function" || typeof value2 === "function") {
91+
return false;
92+
}
93+
94+
if (Array.isArray(value1)) {
95+
if (!Array.isArray(value2)) {
96+
return false;
97+
}
98+
if (value1.length !== value2.length) {
99+
return false;
100+
}
101+
return value1.every((el, i) => this.#areValuesDeepEqual(el, value2[i]));
102+
}
103+
104+
const obj1 = value1 as Record<string, unknown>;
105+
const obj2 = value1 as Record<string, unknown>;
106+
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
107+
return false;
108+
}
109+
for (const key in obj1) {
110+
if (!this.#areValuesDeepEqual(obj1[key], obj2[key])) {
111+
return false;
112+
}
113+
}
114+
115+
return true;
116+
}
117+
71118
// All functions that are part of the public interface must be defined as
72-
// arrow functions, so that they work properly with React
119+
// arrow functions, so they can be passed around React without losing their
120+
// `this` context
73121

74122
getTimeSnapshot = () => {
75123
return this.#timeSync.getTimeSnapshot();
@@ -123,14 +171,16 @@ class ReactTimeSync implements ReactTimeSyncApi {
123171
};
124172

125173
invalidateSelection = (id: string, select?: SelectCallback): void => {
126-
const cacheEntry = this.#selectionCache.get(id);
127-
if (cacheEntry === undefined) {
174+
const prevSelection = this.#selectionCache.get(id);
175+
if (prevSelection === undefined) {
128176
return;
129177
}
130178

131179
const dateSnapshot = this.#timeSync.getTimeSnapshot();
132180
const newSelection = select?.(dateSnapshot) ?? dateSnapshot;
133-
this.#selectionCache.set(id, { value: newSelection });
181+
if (!this.#areValuesDeepEqual(newSelection, prevSelection.value)) {
182+
this.#selectionCache.set(id, { value: newSelection });
183+
}
134184
};
135185
}
136186

@@ -214,42 +264,8 @@ export function useTimeSync(options: UseTimeSyncOptions): Date {
214264
return snapshot;
215265
}
216266

217-
function areDepsInvalidated(
218-
oldDeps: readonly unknown[] | undefined,
219-
newDeps: readonly unknown[] | undefined,
220-
): boolean {
221-
if (oldDeps === undefined) {
222-
if (newDeps === undefined) {
223-
return false;
224-
}
225-
return true;
226-
}
227-
228-
const oldRecast = oldDeps as readonly unknown[];
229-
const newRecast = oldDeps as readonly unknown[];
230-
if (oldRecast.length !== newRecast.length) {
231-
return true;
232-
}
233-
234-
for (const [index, el] of oldRecast.entries()) {
235-
if (el !== newRecast[index]) {
236-
return true;
237-
}
238-
}
239-
240-
return false;
241-
}
242-
243267
type UseTimeSyncSelectOptions<T> = Readonly<
244268
UseTimeSyncOptions & {
245-
/**
246-
* selectDependencies acts like the dependency array for a useMemo
247-
* callback. Whenever any of the elements in the array change by value,
248-
* that will cause the select callback to re-run synchronously and
249-
* produce a new, up-to-date value for the current render.
250-
*/
251-
selectDependencies: readonly unknown[];
252-
253269
/**
254270
* Allows you to transform any date values received from the TimeSync
255271
* class. Select functions work similarly to the selects from React
@@ -279,7 +295,7 @@ type UseTimeSyncSelectOptions<T> = Readonly<
279295
* interval.
280296
*/
281297
export function useTimeSyncSelect<T>(options: UseTimeSyncSelectOptions<T>): T {
282-
const { select, selectDependencies, targetRefreshInterval } = options;
298+
const { select, targetRefreshInterval } = options;
283299
const hookId = useId();
284300
const timeSync = useTimeSyncContext();
285301

@@ -296,7 +312,7 @@ export function useTimeSyncSelect<T>(options: UseTimeSyncSelectOptions<T>): T {
296312
// be initialized with whatever callback you give it on mount. So for the
297313
// mounting render alone, it's safe to call a useEffectEvent callback from
298314
// inside a render.
299-
const selectForOutsideReact = useEffectEvent((date: Date): T => {
315+
const externalSelect = useEffectEvent((date: Date): T => {
300316
const recast = date as Date & T;
301317
return select?.(recast) ?? recast;
302318
});
@@ -314,31 +330,20 @@ export function useTimeSyncSelect<T>(options: UseTimeSyncSelectOptions<T>): T {
314330
targetRefreshInterval,
315331
id: hookId,
316332
onUpdate: notifyReact,
317-
select: selectForOutsideReact,
333+
select: externalSelect,
318334
});
319335
},
320-
[timeSync, hookId, selectForOutsideReact, targetRefreshInterval],
336+
[timeSync, hookId, externalSelect, targetRefreshInterval],
321337
);
322338

323-
const [prevDeps, setPrevDeps] = useState(selectDependencies);
324-
const depsAreInvalidated = areDepsInvalidated(prevDeps, selectDependencies);
325-
326339
const selection = useSyncExternalStore<T>(subscribe, () => {
327-
if (depsAreInvalidated) {
328-
// Need to make sure that we use the un-memoized version of select
329-
// here because we need to call select callback mid-render to
330-
// guarantee no stale data. The memoized version only syncs AFTER
331-
// the current render has finished in full.
332-
timeSync.invalidateSelection(hookId, select);
333-
}
340+
// Need to make sure that we use the un-memoized version of select
341+
// here because we need to call select callback mid-render to
342+
// guarantee no stale data. The memoized version only syncs AFTER
343+
// the current render has finished in full.
344+
timeSync.invalidateSelection(hookId, select);
334345
return timeSync.getSelectionSnapshot(hookId);
335346
});
336347

337-
// Setting state mid-render like this is valid, but we just need to make
338-
// sure that we wait until after the useSyncExternalStore state getter runs
339-
if (depsAreInvalidated) {
340-
setPrevDeps(selectDependencies);
341-
}
342-
343348
return selection;
344349
}

site/src/pages/CliInstallPage/CliInstallPageView.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ type CliInstallPageViewProps = {
1212
export const CliInstallPageView: FC<CliInstallPageViewProps> = ({ origin }) => {
1313
const year = useTimeSyncSelect({
1414
targetRefreshInterval: Number.POSITIVE_INFINITY,
15-
selectDependencies: [],
1615
select: (date) => date.getFullYear(),
1716
});
1817

site/src/pages/LoginPage/LoginPageView.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
3030
}) => {
3131
const year = useTimeSyncSelect({
3232
targetRefreshInterval: Number.POSITIVE_INFINITY,
33-
selectDependencies: [],
3433
select: (date) => date.getFullYear(),
3534
});
3635
const location = useLocation();

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,14 @@ export default function TemplateInsightsPage() {
6969
const { template } = useTemplateLayoutContext();
7070
const [searchParams, setSearchParams] = useSearchParams();
7171

72-
const paramsInterval = searchParams.get("interval");
7372
const insightsInterval = useTimeSyncSelect<InsightsInterval>({
7473
targetRefreshInterval: TARGET_REFRESH_ONE_DAY,
75-
selectDependencies: [paramsInterval],
7674
select: (newDate) => {
7775
const templateCreateDate = new Date(template.created_at);
7876
const hasFiveWeeksOrMore = addWeeks(templateCreateDate, 5) < newDate;
7977
const defaultInterval = hasFiveWeeksOrMore ? "week" : "day";
8078

79+
const paramsInterval = searchParams.get("interval");
8180
if (paramsInterval === "week" || paramsInterval === "day") {
8281
return paramsInterval;
8382
}

site/src/pages/WorkspacePage/AppStatuses.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ export const AppStatuses: FC<AppStatusesProps> = ({
158158
}) => {
159159
const comparisonDate = useTimeSyncSelect({
160160
targetRefreshInterval: TARGET_REFRESH_ONE_MINUTE,
161-
selectDependencies: [referenceDate],
162161
select: (dateState) => referenceDate ?? dateState,
163162
});
164163
const theme = useTheme();

site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ const AutostopDisplay: FC<AutostopDisplayProps> = ({
164164

165165
const activityStatus = useTimeSyncSelect({
166166
targetRefreshInterval: 1_000,
167-
selectDependencies: [workspace],
168167
select: (currentDate) => getWorkspaceActivityStatus(workspace, currentDate),
169168
});
170169
const { message, tooltip, danger } = autostopDisplay(

0 commit comments

Comments
 (0)