Skip to content

Commit a2cc315

Browse files
authored
perf(webapp): stabilize nested component identities (#4689)
## Summary Keep component and renderer identities stable across dashboard renders. Inline icon components, chart renderers, table cells, and select render callbacks now use module-level implementations. Oxlint enforces the pattern across the dashboard. Base: [#4688](#4688)
1 parent 108f43e commit a2cc315

12 files changed

Lines changed: 139 additions & 76 deletions

File tree

.oxlintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
{
116116
"files": ["apps/webapp/app/**/*.ts", "apps/webapp/app/**/*.tsx"],
117117
"rules": {
118+
"react/no-unstable-nested-components": "error",
118119
"react/rules-of-hooks": "error",
119120
"trigger-runops/no-control-plane-run-graph-access": "error",
120121
"trigger-runops/no-control-plane-in-runops-slot": "error"

apps/webapp/app/components/code/TSQLResultsTable.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ const DebouncedInput = forwardRef<
241241
interface ColumnMeta {
242242
outputColumn: OutputColumnMetadata;
243243
alignment: "left" | "right";
244+
prettyFormatting: boolean;
244245
}
245246

246247
/**
@@ -489,6 +490,19 @@ function CellValueWrapper({
489490
/**
490491
* Render a cell value based on its type and optional customRenderType
491492
*/
493+
function TSQLResultsCell(info: CellContext<RowData, unknown>) {
494+
const meta = info.column.columnDef.meta as ColumnMeta;
495+
496+
return (
497+
<CellValueWrapper
498+
value={info.getValue()}
499+
column={meta.outputColumn}
500+
prettyFormatting={meta.prettyFormatting}
501+
row={info.row.original}
502+
/>
503+
);
504+
}
505+
492506
function CellValue({
493507
value,
494508
column,
@@ -1053,17 +1067,11 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
10531067
id: col.name,
10541068
accessorKey: col.name,
10551069
header: () => col.name,
1056-
cell: (info: CellContext<RowData, unknown>) => (
1057-
<CellValueWrapper
1058-
value={info.getValue()}
1059-
column={col}
1060-
prettyFormatting={prettyFormatting}
1061-
row={info.row.original}
1062-
/>
1063-
),
1070+
cell: TSQLResultsCell,
10641071
meta: {
10651072
outputColumn: col,
10661073
alignment: isRightAlignedColumn(col) ? "right" : "left",
1074+
prettyFormatting,
10671075
} as ColumnMeta,
10681076
size: calculateColumnWidth(col.name, rows, col),
10691077
filterFn: fuzzyFilter,

apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ export const ErrorAlertsFormSchema = z.object({
4242
}, z.string().url().array()),
4343
});
4444

45+
type SlackChannel = { id?: string; name?: string; is_private?: boolean };
46+
47+
function renderSlackChannel(channels: SlackChannel[], value: string) {
48+
const channel = channels.find((channel) => value === `${channel.id}/${channel.name}`);
49+
if (!channel) return;
50+
51+
return (
52+
<span className="text-text-bright">
53+
<SlackChannelTitle {...channel} />
54+
</span>
55+
);
56+
}
57+
4558
type ConfigureErrorAlertsProps = ErrorAlertChannelData & {
4659
connectToSlackHref?: string;
4760
formAction: string;
@@ -196,15 +209,7 @@ export function ConfigureErrorAlerts({
196209
filter={(channel, search) =>
197210
channel.name?.toLowerCase().includes(search.toLowerCase()) ?? false
198211
}
199-
text={(value) => {
200-
const channel = slack.channels.find((s) => value === `${s.id}/${s.name}`);
201-
if (!channel) return;
202-
return (
203-
<span className="text-text-bright">
204-
<SlackChannelTitle {...channel} />
205-
</span>
206-
);
207-
}}
212+
text={(value) => renderSlackChannel(slack.channels, value)}
208213
>
209214
{(matches) => (
210215
<>

apps/webapp/app/components/primitives/charts/ChartLine.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,8 @@ export function ChartLineRenderer({
578578
// own dot on top where it's active.
579579
activeDot={
580580
gradientLine
581-
? (props: ActiveDotProps) => (
581+
? // oxlint-disable-next-line react/no-unstable-nested-components -- Recharts invokes this renderer with hover coordinates; an element would rely on cloneElement prop injection.
582+
(props: ActiveDotProps) => (
582583
<ThresholdActiveDot
583584
{...props}
584585
dataKey={key}

apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ function ReplayContent({ runFriendlyId, failedRedirect }: ReplayRunDialogProps)
124124
const startingJson = "{\n\n}";
125125
const machinePresets = Object.values(MachinePresetName.enum);
126126

127+
type ReplayEnvironment = UseDataFunctionReturn<typeof loader>["environments"][number];
128+
129+
function renderReplayEnvironment(
130+
environments: ReplayEnvironment[],
131+
value: string
132+
): React.ReactNode {
133+
const environment = environments.find((environment) => environment.id === value);
134+
if (!environment) return;
135+
136+
return (
137+
<div className="flex items-center pl-1 pr-2">
138+
<EnvironmentCombo environment={environment} />
139+
</div>
140+
);
141+
}
142+
127143
function ReplayForm({
128144
failedRedirect,
129145
runFriendlyId,
@@ -572,14 +588,7 @@ function ReplayForm({
572588
(item) => item.branchName?.replace(/\//g, " ").replace(/_/g, " ") ?? "",
573589
],
574590
}}
575-
text={(value) => {
576-
const env = replayData.environments.find((env) => env.id === value)!;
577-
return (
578-
<div className="flex items-center pl-1 pr-2">
579-
<EnvironmentCombo environment={env} />
580-
</div>
581-
);
582-
}}
591+
text={(value) => renderReplayEnvironment(replayData.environments, value)}
583592
>
584593
{(matches) =>
585594
matches.map((env) => (

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ import {
4848
} from "~/v3/services/alerts/safeWebhookUrl.server";
4949
import { pageMeta } from "~/utils/pageTitle";
5050

51+
type SlackChannel = { id?: string; name?: string; is_private?: boolean };
52+
53+
function renderSlackChannel(channels: SlackChannel[], value: string | string[]) {
54+
if (typeof value !== "string") return;
55+
const channel = channels.find((channel) => value === `${channel.id}/${channel.name}`);
56+
return channel ? <SlackChannelTitle {...channel} /> : undefined;
57+
}
58+
5159
export const meta = pageMeta("New alert");
5260

5361
const FormSchema = z
@@ -342,11 +350,7 @@ export default function Page() {
342350
filter={(channel, search) =>
343351
channel.name?.toLowerCase().includes(search.toLowerCase()) ?? false
344352
}
345-
text={(value) => {
346-
const channel = slack.channels.find((s) => value === `${s.id}/${s.name}`);
347-
if (!channel) return;
348-
return <SlackChannelTitle {...channel} />;
349-
}}
353+
text={(value) => renderSlackChannel(slack.channels, value)}
350354
>
351355
{(matches) => (
352356
<>

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ import { Form, useFetcher, useRevalidator } from "@remix-run/react";
44
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
55
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
66
import { type ErrorGroupStatus } from "@trigger.dev/database";
7-
import { Suspense, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
7+
import {
8+
Suspense,
9+
useCallback,
10+
useEffect,
11+
useMemo,
12+
useRef,
13+
useState,
14+
type ComponentProps,
15+
type ReactNode,
16+
} from "react";
817
import {
918
Bar,
1019
BarChart,
@@ -628,6 +637,17 @@ function ErrorGroupRow({
628637
);
629638
}
630639

640+
function renderErrorActionsPopoverContent(props: ComponentProps<typeof ErrorStatusMenuItems>) {
641+
return (
642+
<>
643+
<PopoverSectionHeader title="Mark error as…" />
644+
<div className="flex flex-col gap-1 p-1">
645+
<ErrorStatusMenuItems {...props} />
646+
</div>
647+
</>
648+
);
649+
}
650+
631651
function ErrorActionsCell({
632652
errorGroup,
633653
organizationSlug,
@@ -664,26 +684,21 @@ function ErrorActionsCell({
664684
<>
665685
<TableCellMenu
666686
isSticky
667-
popoverContent={(close) => (
668-
<>
669-
<PopoverSectionHeader title="Mark error as…" />
670-
<div className="flex flex-col gap-1 p-1">
671-
<ErrorStatusMenuItems
672-
status={errorGroup.status}
673-
taskIdentifier={errorGroup.taskIdentifier}
674-
onAction={(data) => {
675-
close();
676-
pendingToast.current = statusActionToastMessage(data);
677-
fetcher.submit(data, { method: "post", action: actionUrl });
678-
}}
679-
onCustomIgnore={() => {
680-
close();
681-
setCustomIgnoreOpen(true);
682-
}}
683-
/>
684-
</div>
685-
</>
686-
)}
687+
popoverContent={(close) =>
688+
renderErrorActionsPopoverContent({
689+
status: errorGroup.status,
690+
taskIdentifier: errorGroup.taskIdentifier,
691+
onAction: (data) => {
692+
close();
693+
pendingToast.current = statusActionToastMessage(data);
694+
fetcher.submit(data, { method: "post", action: actionUrl });
695+
},
696+
onCustomIgnore: () => {
697+
close();
698+
setCustomIgnoreOpen(true);
699+
},
700+
})
701+
}
687702
/>
688703
<CustomIgnoreDialog
689704
open={customIgnoreOpen}

apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ function shuffleArray<T>(arr: T[]): T[] {
7676
return shuffled;
7777
}
7878

79+
function renderMultiSelectValue(value: string[]) {
80+
if (value.length === 0) return;
81+
82+
return (
83+
<span className="flex min-w-0 items-center text-text-bright">
84+
<span className="truncate">{value.slice(0, 2).join(", ")}</span>
85+
{value.length > 2 && <span className="ml-1 flex-none">+{value.length - 2} more</span>}
86+
</span>
87+
);
88+
}
89+
7990
function MultiSelectField({
8091
value,
8192
setValue,
@@ -97,14 +108,7 @@ function MultiSelectField({
97108
icon={icon}
98109
items={items}
99110
className="h-8 min-w-0 border-0 bg-background-hover pl-2 text-sm text-text-dimmed ring-border-bright transition hover:bg-secondary hover:text-text-dimmed hover:ring-1"
100-
text={(v) =>
101-
v.length === 0 ? undefined : (
102-
<span className="flex min-w-0 items-center text-text-bright">
103-
<span className="truncate">{v.slice(0, 2).join(", ")}</span>
104-
{v.length > 2 && <span className="ml-1 flex-none">+{v.length - 2} more</span>}
105-
</span>
106-
)
107-
}
111+
text={renderMultiSelectValue}
108112
>
109113
{(items) =>
110114
items.map((item) => (

apps/webapp/app/routes/account._index/route.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ function themeIcon(value: ThemePreference) {
7070
}
7171
}
7272

73+
function renderTheme(value: ThemePreference) {
74+
return (
75+
<span className="flex items-center gap-1.5">
76+
{themeIcon(value)}
77+
{themeLabel(value)}
78+
</span>
79+
);
80+
}
81+
7382
export const meta = pageMeta("Your profile");
7483

7584
function createSchema(
@@ -320,12 +329,7 @@ export default function Page() {
320329
variant="secondary/small"
321330
dropdownIcon
322331
items={["classic", "system", "dark", "light"]}
323-
text={(value) => (
324-
<span className="flex items-center gap-1.5">
325-
{themeIcon(value)}
326-
{themeLabel(value)}
327-
</span>
328-
)}
332+
text={renderTheme}
329333
className="w-44"
330334
>
331335
{(items) =>

apps/webapp/app/routes/confirm-basic-details.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ const HandIcon = forwardRef<HTMLDivElement, {}>(({}, ref) => {
207207
});
208208
const MotionHand = motion(HandIcon);
209209

210+
function renderRole(value: string) {
211+
return value ? <span className="text-text-bright">{value}</span> : undefined;
212+
}
213+
210214
export default function Page() {
211215
const user = useUser();
212216
const lastSubmission = useActionData();
@@ -390,7 +394,7 @@ export default function Page() {
390394
icon={<UserGroupIcon className="mr-1 size-4.5 text-text-dimmed" />}
391395
items={shuffledRoles}
392396
className="h-8 min-w-0 border-0 bg-background-hover pl-2 text-sm text-text-dimmed ring-border-bright transition hover:bg-secondary hover:text-text-dimmed hover:ring-1"
393-
text={(v) => (v ? <span className="text-text-bright">{v}</span> : undefined)}
397+
text={renderRole}
394398
>
395399
{(items) =>
396400
items.map((item) => (

0 commit comments

Comments
 (0)