Skip to content

Commit b89904c

Browse files
committed
simplify props destructuring in AsyncContent and LeaderboardPage components
1 parent c147efe commit b89904c

3 files changed

Lines changed: 94 additions & 55 deletions

File tree

frontend/__tests__/components/common/AsyncContent.spec.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -381,17 +381,14 @@ describe("AsyncContent", () => {
381381
{...(options as Props<Q>)}
382382
alwaysShowContent
383383
>
384-
{(results) => (
384+
{({ first, second }) => (
385385
<>
386386
<Show
387-
when={
388-
results()?.first !== undefined &&
389-
results()?.second !== undefined
390-
}
387+
when={first() !== undefined && second() !== undefined}
391388
fallback={<div>no data</div>}
392389
>
393-
<div data-testid="first">{results()?.first}</div>
394-
<div data-testid="second">{results()?.second}</div>
390+
<div data-testid="first">{first()}</div>
391+
<div data-testid="second">{second()}</div>
395392
</Show>
396393
</>
397394
)}
@@ -405,17 +402,14 @@ describe("AsyncContent", () => {
405402
{...(options as Props<Q>)}
406403
alwaysShowContent={false}
407404
>
408-
{(results) => (
405+
{({ first, second }) => (
409406
<>
410407
<Show
411-
when={
412-
results().first !== undefined &&
413-
results().second !== undefined
414-
}
408+
when={first() !== undefined && second() !== undefined}
415409
fallback={<div>no data</div>}
416410
>
417-
<div data-testid="first">{results().first}</div>
418-
<div data-testid="second">{results().second}</div>
411+
<div data-testid="first">{first()}</div>
412+
<div data-testid="second">{second()}</div>
419413
</Show>
420414
</>
421415
)}

frontend/src/ts/components/common/AsyncContent.tsx

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ type SingleCollectionProps<T> = {
5454
collection: Collection<T>;
5555
};
5656

57+
type AccessorMap<T> = { [K in keyof T]: Accessor<T[K]> };
58+
5759
type DeferredChildren<T extends QueryMapping> = {
5860
alwaysShowContent?: false;
5961
children: (data: Accessor<{ [K in keyof T]: T[K] }>) => JSXElement;
@@ -67,18 +69,39 @@ type EagerChildren<T extends QueryMapping> = {
6769
) => JSXElement;
6870
};
6971

72+
type MultiDeferredChildren<T extends QueryMapping> = {
73+
alwaysShowContent?: false;
74+
children: (data: AccessorMap<{ [K in keyof T]: T[K] }>) => JSXElement;
75+
};
76+
77+
type MultiEagerChildren<T extends QueryMapping> = {
78+
alwaysShowContent: true;
79+
showLoader?: true;
80+
children: (
81+
data: AccessorMap<{ [K in keyof T]: T[K] | undefined }>,
82+
) => JSXElement;
83+
};
84+
85+
type SingleSource<T> = SingleQueryProps<T> | SingleCollectionProps<T>;
86+
type MultiSource<T extends QueryMapping> = QueryProps<T> | CollectionProps<T>;
87+
type SingleChildren<T> = DeferredChildren<T> | EagerChildren<T>;
88+
type MultiChildren<T extends QueryMapping> =
89+
| MultiDeferredChildren<T>
90+
| MultiEagerChildren<T>;
91+
7092
export type Props<T extends QueryMapping> = BaseProps &
71-
(
72-
| QueryProps<T>
73-
| SingleQueryProps<T>
74-
| CollectionProps<T>
75-
| SingleCollectionProps<T>
76-
) &
77-
(DeferredChildren<T> | EagerChildren<T>);
78-
79-
export default function AsyncContent<T extends QueryMapping>(
80-
props: Props<T>,
81-
): JSXElement {
93+
(SingleSource<T> | MultiSource<T>) &
94+
(SingleChildren<T> | MultiChildren<T>);
95+
96+
// Single query/collection overloads
97+
function AsyncContent<T>(
98+
props: BaseProps & SingleSource<T> & SingleChildren<T>,
99+
): JSXElement;
100+
// Multi query/collection overloads
101+
function AsyncContent<T extends Record<string, unknown>>(
102+
props: BaseProps & MultiSource<T> & MultiChildren<T>,
103+
): JSXElement;
104+
function AsyncContent<T extends QueryMapping>(props: Props<T>): JSXElement {
82105
//@ts-expect-error this is fine
83106
const source = createMemo<AsyncMap<T>>(() => {
84107
if ("query" in props) {
@@ -150,6 +173,27 @@ export default function AsyncContent<T extends QueryMapping>(
150173
false,
151174
);
152175

176+
// Keys are stable for the component lifetime; per-key closures track
177+
// reactivity internally via value()/lastResolvedValue().
178+
// oxlint-disable solid/reactivity
179+
const multi = !("defaultQuery" in source());
180+
181+
const eagerAccessorMap = multi
182+
? (Object.fromEntries(
183+
typedKeys(source()).map((key) => [key, () => value()?.[key]]),
184+
) as AccessorMap<{ [K in keyof T]: T[K] | undefined }>)
185+
: undefined;
186+
187+
const deferredAccessorMap = multi
188+
? (Object.fromEntries(
189+
typedKeys(source()).map((key) => [
190+
key,
191+
() => lastResolvedValue()?.[key],
192+
]),
193+
) as AccessorMap<{ [K in keyof T]: T[K] }>)
194+
: undefined;
195+
// oxlint-enable solid/reactivity
196+
153197
const loader = (): JSXElement =>
154198
props.loader ?? <LoadingCircle class="p-4 text-center text-2xl" />;
155199

@@ -175,14 +219,18 @@ export default function AsyncContent<T extends QueryMapping>(
175219
fallback={
176220
<Show when={hasResolved()}>
177221
{(_) =>
178-
props.children(
179-
lastResolvedValue as Accessor<{ [K in keyof T]: T[K] }>,
222+
// oxlint-disable-next-line typescript/no-explicit-any
223+
(props.children as (data: any) => JSXElement)(
224+
multi ? deferredAccessorMap : lastResolvedValue,
180225
)
181226
}
182227
</Show>
183228
}
184229
>
185-
{props.children(value)}
230+
{/* oxlint-disable-next-line typescript/no-explicit-any */}
231+
{(props.children as (data: any) => JSXElement)(
232+
multi ? eagerAccessorMap : value,
233+
)}
186234
</Show>
187235
</>
188236
}
@@ -225,3 +273,5 @@ function fromCollections<T extends Record<string, unknown>>(collections: {
225273
return acc;
226274
}, {} as AsyncMap<T>);
227275
}
276+
277+
export default AsyncContent;

frontend/src/ts/components/pages/leaderboard/LeaderboardPage.tsx

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -203,33 +203,28 @@ export function LeaderboardPage(): JSXElement {
203203
alwaysShowContent
204204
errorClass="rounded bg-sub-alt p-4"
205205
>
206-
{(queries) => {
207-
const data = () => queries()?.data;
208-
const rank = () => queries()?.rank;
209-
const config = () => queries()?.config;
210-
return (
211-
<UserRank
212-
type={getSelection().type === "weekly" ? "xp" : "speed"}
213-
data={rank()}
214-
friendsOnly={getSelection().friendsOnly}
215-
total={data()?.count}
216-
minWpm={(() => {
217-
const d = data();
218-
return d && "minWpm" in d
219-
? (d.minWpm as number)
220-
: undefined;
221-
})()}
222-
memoryDifference={getLbMemoryDifference(
223-
getSelection(),
224-
rank()?.rank,
225-
)}
226-
isLbOptOut={getSnapshot()?.lbOptOut ?? false}
227-
isBanned={getSnapshot()?.banned ?? false}
228-
minTimeTyping={config()?.leaderboards.minTimeTyping ?? 0}
229-
userTimeTyping={getSnapshot()?.typingStats.timeTyping ?? 0}
230-
/>
231-
);
232-
}}
206+
{({ data, rank, config }) => (
207+
<UserRank
208+
type={getSelection().type === "weekly" ? "xp" : "speed"}
209+
data={rank()}
210+
friendsOnly={getSelection().friendsOnly}
211+
total={data()?.count}
212+
minWpm={(() => {
213+
const d = data();
214+
return d && "minWpm" in d
215+
? (d.minWpm as number)
216+
: undefined;
217+
})()}
218+
memoryDifference={getLbMemoryDifference(
219+
getSelection(),
220+
rank()?.rank,
221+
)}
222+
isLbOptOut={getSnapshot()?.lbOptOut ?? false}
223+
isBanned={getSnapshot()?.banned ?? false}
224+
minTimeTyping={config()?.leaderboards.minTimeTyping ?? 0}
225+
userTimeTyping={getSnapshot()?.typingStats.timeTyping ?? 0}
226+
/>
227+
)}
233228
</AsyncContent>
234229
</Show>
235230

0 commit comments

Comments
 (0)