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](https://github.com/triggerdotdev/trigger.dev/pull/4688)
This commit is contained in:
Chris Arderne
2026-08-19 16:35:41 +01:00
committed by GitHub
parent 108f43ee9b
commit a2cc315f40
12 changed files with 139 additions and 76 deletions
+1
View File
@@ -115,6 +115,7 @@
{
"files": ["apps/webapp/app/**/*.ts", "apps/webapp/app/**/*.tsx"],
"rules": {
"react/no-unstable-nested-components": "error",
"react/rules-of-hooks": "error",
"trigger-runops/no-control-plane-run-graph-access": "error",
"trigger-runops/no-control-plane-in-runops-slot": "error"
@@ -241,6 +241,7 @@ const DebouncedInput = forwardRef<
interface ColumnMeta {
outputColumn: OutputColumnMetadata;
alignment: "left" | "right";
prettyFormatting: boolean;
}
/**
@@ -489,6 +490,19 @@ function CellValueWrapper({
/**
* Render a cell value based on its type and optional customRenderType
*/
function TSQLResultsCell(info: CellContext<RowData, unknown>) {
const meta = info.column.columnDef.meta as ColumnMeta;
return (
<CellValueWrapper
value={info.getValue()}
column={meta.outputColumn}
prettyFormatting={meta.prettyFormatting}
row={info.row.original}
/>
);
}
function CellValue({
value,
column,
@@ -1053,17 +1067,11 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
id: col.name,
accessorKey: col.name,
header: () => col.name,
cell: (info: CellContext<RowData, unknown>) => (
<CellValueWrapper
value={info.getValue()}
column={col}
prettyFormatting={prettyFormatting}
row={info.row.original}
/>
),
cell: TSQLResultsCell,
meta: {
outputColumn: col,
alignment: isRightAlignedColumn(col) ? "right" : "left",
prettyFormatting,
} as ColumnMeta,
size: calculateColumnWidth(col.name, rows, col),
filterFn: fuzzyFilter,
@@ -42,6 +42,19 @@ export const ErrorAlertsFormSchema = z.object({
}, z.string().url().array()),
});
type SlackChannel = { id?: string; name?: string; is_private?: boolean };
function renderSlackChannel(channels: SlackChannel[], value: string) {
const channel = channels.find((channel) => value === `${channel.id}/${channel.name}`);
if (!channel) return;
return (
<span className="text-text-bright">
<SlackChannelTitle {...channel} />
</span>
);
}
type ConfigureErrorAlertsProps = ErrorAlertChannelData & {
connectToSlackHref?: string;
formAction: string;
@@ -196,15 +209,7 @@ export function ConfigureErrorAlerts({
filter={(channel, search) =>
channel.name?.toLowerCase().includes(search.toLowerCase()) ?? false
}
text={(value) => {
const channel = slack.channels.find((s) => value === `${s.id}/${s.name}`);
if (!channel) return;
return (
<span className="text-text-bright">
<SlackChannelTitle {...channel} />
</span>
);
}}
text={(value) => renderSlackChannel(slack.channels, value)}
>
{(matches) => (
<>
@@ -578,7 +578,8 @@ export function ChartLineRenderer({
// own dot on top where it's active.
activeDot={
gradientLine
? (props: ActiveDotProps) => (
? // oxlint-disable-next-line react/no-unstable-nested-components -- Recharts invokes this renderer with hover coordinates; an element would rely on cloneElement prop injection.
(props: ActiveDotProps) => (
<ThresholdActiveDot
{...props}
dataKey={key}
@@ -124,6 +124,22 @@ function ReplayContent({ runFriendlyId, failedRedirect }: ReplayRunDialogProps)
const startingJson = "{\n\n}";
const machinePresets = Object.values(MachinePresetName.enum);
type ReplayEnvironment = UseDataFunctionReturn<typeof loader>["environments"][number];
function renderReplayEnvironment(
environments: ReplayEnvironment[],
value: string
): React.ReactNode {
const environment = environments.find((environment) => environment.id === value);
if (!environment) return;
return (
<div className="flex items-center pl-1 pr-2">
<EnvironmentCombo environment={environment} />
</div>
);
}
function ReplayForm({
failedRedirect,
runFriendlyId,
@@ -572,14 +588,7 @@ function ReplayForm({
(item) => item.branchName?.replace(/\//g, " ").replace(/_/g, " ") ?? "",
],
}}
text={(value) => {
const env = replayData.environments.find((env) => env.id === value)!;
return (
<div className="flex items-center pl-1 pr-2">
<EnvironmentCombo environment={env} />
</div>
);
}}
text={(value) => renderReplayEnvironment(replayData.environments, value)}
>
{(matches) =>
matches.map((env) => (
@@ -48,6 +48,14 @@ import {
} from "~/v3/services/alerts/safeWebhookUrl.server";
import { pageMeta } from "~/utils/pageTitle";
type SlackChannel = { id?: string; name?: string; is_private?: boolean };
function renderSlackChannel(channels: SlackChannel[], value: string | string[]) {
if (typeof value !== "string") return;
const channel = channels.find((channel) => value === `${channel.id}/${channel.name}`);
return channel ? <SlackChannelTitle {...channel} /> : undefined;
}
export const meta = pageMeta("New alert");
const FormSchema = z
@@ -342,11 +350,7 @@ export default function Page() {
filter={(channel, search) =>
channel.name?.toLowerCase().includes(search.toLowerCase()) ?? false
}
text={(value) => {
const channel = slack.channels.find((s) => value === `${s.id}/${s.name}`);
if (!channel) return;
return <SlackChannelTitle {...channel} />;
}}
text={(value) => renderSlackChannel(slack.channels, value)}
>
{(matches) => (
<>
@@ -4,7 +4,16 @@ import { Form, useFetcher, useRevalidator } from "@remix-run/react";
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
import { type ErrorGroupStatus } from "@trigger.dev/database";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import {
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
type ComponentProps,
type ReactNode,
} from "react";
import {
Bar,
BarChart,
@@ -628,6 +637,17 @@ function ErrorGroupRow({
);
}
function renderErrorActionsPopoverContent(props: ComponentProps<typeof ErrorStatusMenuItems>) {
return (
<>
<PopoverSectionHeader title="Mark error as…" />
<div className="flex flex-col gap-1 p-1">
<ErrorStatusMenuItems {...props} />
</div>
</>
);
}
function ErrorActionsCell({
errorGroup,
organizationSlug,
@@ -664,26 +684,21 @@ function ErrorActionsCell({
<>
<TableCellMenu
isSticky
popoverContent={(close) => (
<>
<PopoverSectionHeader title="Mark error as…" />
<div className="flex flex-col gap-1 p-1">
<ErrorStatusMenuItems
status={errorGroup.status}
taskIdentifier={errorGroup.taskIdentifier}
onAction={(data) => {
close();
pendingToast.current = statusActionToastMessage(data);
fetcher.submit(data, { method: "post", action: actionUrl });
}}
onCustomIgnore={() => {
close();
setCustomIgnoreOpen(true);
}}
/>
</div>
</>
)}
popoverContent={(close) =>
renderErrorActionsPopoverContent({
status: errorGroup.status,
taskIdentifier: errorGroup.taskIdentifier,
onAction: (data) => {
close();
pendingToast.current = statusActionToastMessage(data);
fetcher.submit(data, { method: "post", action: actionUrl });
},
onCustomIgnore: () => {
close();
setCustomIgnoreOpen(true);
},
})
}
/>
<CustomIgnoreDialog
open={customIgnoreOpen}
@@ -76,6 +76,17 @@ function shuffleArray<T>(arr: T[]): T[] {
return shuffled;
}
function renderMultiSelectValue(value: string[]) {
if (value.length === 0) return;
return (
<span className="flex min-w-0 items-center text-text-bright">
<span className="truncate">{value.slice(0, 2).join(", ")}</span>
{value.length > 2 && <span className="ml-1 flex-none">+{value.length - 2} more</span>}
</span>
);
}
function MultiSelectField({
value,
setValue,
@@ -97,14 +108,7 @@ function MultiSelectField({
icon={icon}
items={items}
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"
text={(v) =>
v.length === 0 ? undefined : (
<span className="flex min-w-0 items-center text-text-bright">
<span className="truncate">{v.slice(0, 2).join(", ")}</span>
{v.length > 2 && <span className="ml-1 flex-none">+{v.length - 2} more</span>}
</span>
)
}
text={renderMultiSelectValue}
>
{(items) =>
items.map((item) => (
@@ -70,6 +70,15 @@ function themeIcon(value: ThemePreference) {
}
}
function renderTheme(value: ThemePreference) {
return (
<span className="flex items-center gap-1.5">
{themeIcon(value)}
{themeLabel(value)}
</span>
);
}
export const meta = pageMeta("Your profile");
function createSchema(
@@ -320,12 +329,7 @@ export default function Page() {
variant="secondary/small"
dropdownIcon
items={["classic", "system", "dark", "light"]}
text={(value) => (
<span className="flex items-center gap-1.5">
{themeIcon(value)}
{themeLabel(value)}
</span>
)}
text={renderTheme}
className="w-44"
>
{(items) =>
@@ -207,6 +207,10 @@ const HandIcon = forwardRef<HTMLDivElement, {}>(({}, ref) => {
});
const MotionHand = motion(HandIcon);
function renderRole(value: string) {
return value ? <span className="text-text-bright">{value}</span> : undefined;
}
export default function Page() {
const user = useUser();
const lastSubmission = useActionData();
@@ -390,7 +394,7 @@ export default function Page() {
icon={<UserGroupIcon className="mr-1 size-4.5 text-text-dimmed" />}
items={shuffledRoles}
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"
text={(v) => (v ? <span className="text-text-bright">{v}</span> : undefined)}
text={renderRole}
>
{(items) =>
items.map((item) => (
@@ -541,6 +541,14 @@ function VercelAppInstalledRow() {
);
}
function VercelLeadingIcon() {
return <VercelLogo className="-mx-1 size-3.5 text-text-bright" />;
}
function VercelLoadingIcon() {
return <Spinner color="blue" className="size-4" />;
}
function VercelSettingsRows({
organizationSlug,
projectSlug,
@@ -577,7 +585,7 @@ function VercelSettingsRows({
noPermissionTooltip={noPermissionTooltip}
to={vercelAppInstallPath(organizationSlug, projectSlug)}
variant="secondary/small"
LeadingIcon={() => <VercelLogo className="-mx-1 size-3.5 text-text-bright" />}
LeadingIcon={VercelLeadingIcon}
>
Install Vercel app
</PermissionLink>
@@ -595,11 +603,7 @@ function VercelSettingsRows({
onClick={() => onOpenModal?.()}
disabled={isLoadingProjects || !onOpenModal || !canManageVercel}
tooltip={canManageVercel ? undefined : noPermissionTooltip}
LeadingIcon={
isLoadingProjects
? () => <Spinner color="blue" className="size-4" />
: () => <VercelLogo className="-mx-1 size-3.5 text-text-bright" />
}
LeadingIcon={isLoadingProjects ? VercelLoadingIcon : VercelLeadingIcon}
>
{isLoadingProjects ? "Loading projects…" : "Connect Vercel project"}
</Button>
@@ -43,6 +43,10 @@ import { sendToPlain } from "~/utils/plain.server";
import { formatCurrency } from "~/utils/numberFormatter";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
function WhiteSpinnerIcon() {
return <Spinner color="white" />;
}
const Params = z.object({
organizationSlug: z.string(),
});
@@ -399,7 +403,7 @@ export function TierFree({
<Button
variant="danger/medium"
disabled={isLoading}
LeadingIcon={isLoading ? () => <Spinner color="white" /> : undefined}
LeadingIcon={isLoading ? WhiteSpinnerIcon : undefined}
type="submit"
>
Downgrade plan
@@ -527,7 +531,7 @@ export function TierHobby({
<Button
variant="secondary/medium"
disabled={isLoading}
LeadingIcon={isLoading ? () => <Spinner color="white" /> : undefined}
LeadingIcon={isLoading ? WhiteSpinnerIcon : undefined}
form="subscribe-hobby"
>
{`Downgrade to ${plan.title}`}
@@ -670,7 +674,7 @@ export function TierPro({
<Button
variant="primary/medium"
disabled={isLoading}
LeadingIcon={isLoading ? () => <Spinner color="white" /> : undefined}
LeadingIcon={isLoading ? WhiteSpinnerIcon : undefined}
form="subscribe-pro"
>
{`Upgrade to ${plan.title}`}