fix(webapp,react-hooks): enforce stable hook ordering (#4688)
## Summary Enforce stable React hook ordering in the dashboard and React hooks package. Conditional hook calls now keep a consistent order, and overloaded realtime stream arguments are resolved before entering the shared hook implementation. Base: `main`
This commit is contained in:
+9
-2
@@ -45,8 +45,8 @@
|
||||
"typescript/consistent-type-imports": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/namespace": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
"react/exhaustive-deps": "off",
|
||||
"react/rules-of-hooks": "off",
|
||||
"guard-for-in": "error",
|
||||
"symbol-description": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
@@ -115,10 +115,17 @@
|
||||
{
|
||||
"files": ["apps/webapp/app/**/*.ts", "apps/webapp/app/**/*.tsx"],
|
||||
"rules": {
|
||||
"react/rules-of-hooks": "error",
|
||||
"trigger-runops/no-control-plane-run-graph-access": "error",
|
||||
"trigger-runops/no-control-plane-in-runops-slot": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/react-hooks/src/**/*.ts", "packages/react-hooks/src/**/*.tsx"],
|
||||
"rules": {
|
||||
"react/rules-of-hooks": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["apps/webapp/app/**/*.test.ts", "apps/webapp/app/**/*.test.tsx"],
|
||||
"rules": {
|
||||
|
||||
@@ -68,17 +68,15 @@ export const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.
|
||||
|
||||
const { container, root, thumb, text } = variations[variant];
|
||||
|
||||
if (props.shortcut) {
|
||||
useShortcutKeys({
|
||||
shortcut: props.shortcut,
|
||||
action: () => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.click();
|
||||
}
|
||||
},
|
||||
disabled: props.disabled,
|
||||
});
|
||||
}
|
||||
useShortcutKeys({
|
||||
shortcut: props.shortcut,
|
||||
action: () => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.click();
|
||||
}
|
||||
},
|
||||
disabled: props.disabled,
|
||||
});
|
||||
|
||||
const labelElement = label ? (
|
||||
<label
|
||||
|
||||
@@ -46,16 +46,14 @@ export function TextLink({
|
||||
const innerRef = useRef<HTMLAnchorElement>(null);
|
||||
const classes = variations[variant];
|
||||
|
||||
if (shortcut) {
|
||||
useShortcutKeys({
|
||||
shortcut: shortcut,
|
||||
action: () => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.click();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
useShortcutKeys({
|
||||
shortcut,
|
||||
action: () => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.click();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const renderShortcutKey = () =>
|
||||
shortcut &&
|
||||
|
||||
+5
-10
@@ -602,12 +602,11 @@ function shouldLiveReload({
|
||||
return true;
|
||||
}
|
||||
|
||||
function TraceView({
|
||||
run,
|
||||
trace,
|
||||
maximumLiveReloadingSetting,
|
||||
resizable,
|
||||
}: Pick<LoaderData, "run" | "trace" | "maximumLiveReloadingSetting" | "resizable">) {
|
||||
type TraceViewProps = Pick<LoaderData, "run" | "maximumLiveReloadingSetting" | "resizable"> & {
|
||||
trace: NonNullable<LoaderData["trace"]>;
|
||||
};
|
||||
|
||||
function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: TraceViewProps) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const environment = useEnvironment();
|
||||
@@ -616,10 +615,6 @@ function TraceView({
|
||||
const frozenSpanId = useFrozenValue(selectedSpanId);
|
||||
const displaySpanId = selectedSpanId ?? frozenSpanId;
|
||||
|
||||
if (!trace) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const {
|
||||
events,
|
||||
duration,
|
||||
|
||||
+7
-7
@@ -295,15 +295,11 @@ export const handle: Handle = {
|
||||
export default function Page() {
|
||||
const result = useTypedLoaderData<typeof loader>();
|
||||
|
||||
if (!result.foundTask) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const params = useParams();
|
||||
const queueFetcher = useFetcher<typeof queuesLoader>();
|
||||
|
||||
useEffect(() => {
|
||||
if (params.organizationSlug && params.projectParam && params.envParam) {
|
||||
if (result.foundTask && params.organizationSlug && params.projectParam && params.envParam) {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set("type", "custom");
|
||||
searchParams.set("per_page", "100");
|
||||
@@ -314,9 +310,9 @@ export default function Page() {
|
||||
}/queues?${searchParams.toString()}`
|
||||
);
|
||||
}
|
||||
}, [params.organizationSlug, params.projectParam, params.envParam]);
|
||||
}, [result.foundTask, params.organizationSlug, params.projectParam, params.envParam]);
|
||||
|
||||
const defaultTaskQueue = "queue" in result ? result.queue : undefined;
|
||||
const defaultTaskQueue = result.foundTask && "queue" in result ? result.queue : undefined;
|
||||
const queues = useMemo(() => {
|
||||
const customQueues = queueFetcher.data?.queues ?? [];
|
||||
|
||||
@@ -325,6 +321,10 @@ export default function Page() {
|
||||
: customQueues;
|
||||
}, [queueFetcher.data?.queues, defaultTaskQueue]);
|
||||
|
||||
if (!result.foundTask) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const { triggerSource } = result;
|
||||
|
||||
switch (triggerSource) {
|
||||
|
||||
@@ -750,33 +750,29 @@ export function useRealtimeStream<TPart>(
|
||||
streamKeyOrOptionsOrRunId?: string | UseRealtimeStreamOptions<TPart>,
|
||||
options?: UseRealtimeStreamOptions<TPart>
|
||||
): UseRealtimeStreamInstance<TPart> {
|
||||
let runId: string;
|
||||
let streamKey: string;
|
||||
let resolvedOptions: UseRealtimeStreamOptions<TPart> | undefined;
|
||||
|
||||
if (typeof runIdOrDefinedStream === "string") {
|
||||
if (typeof streamKeyOrOptionsOrRunId === "string") {
|
||||
return useRealtimeStreamImplementation(
|
||||
runIdOrDefinedStream,
|
||||
streamKeyOrOptionsOrRunId,
|
||||
options
|
||||
);
|
||||
} else {
|
||||
return useRealtimeStreamImplementation(
|
||||
runIdOrDefinedStream,
|
||||
"default",
|
||||
streamKeyOrOptionsOrRunId
|
||||
);
|
||||
}
|
||||
runId = runIdOrDefinedStream;
|
||||
streamKey =
|
||||
typeof streamKeyOrOptionsOrRunId === "string" ? streamKeyOrOptionsOrRunId : "default";
|
||||
resolvedOptions =
|
||||
typeof streamKeyOrOptionsOrRunId === "string" ? options : streamKeyOrOptionsOrRunId;
|
||||
} else {
|
||||
if (typeof streamKeyOrOptionsOrRunId === "string") {
|
||||
return useRealtimeStreamImplementation(
|
||||
streamKeyOrOptionsOrRunId,
|
||||
runIdOrDefinedStream.id,
|
||||
options
|
||||
);
|
||||
} else {
|
||||
if (typeof streamKeyOrOptionsOrRunId !== "string") {
|
||||
throw new Error(
|
||||
"Invalid second argument to useRealtimeStream. When using a defined stream instance, the second argument to useRealtimeStream must be a run ID."
|
||||
);
|
||||
}
|
||||
|
||||
runId = streamKeyOrOptionsOrRunId;
|
||||
streamKey = runIdOrDefinedStream.id;
|
||||
resolvedOptions = options;
|
||||
}
|
||||
|
||||
return useRealtimeStreamImplementation(runId, streamKey, resolvedOptions);
|
||||
}
|
||||
|
||||
function useRealtimeStreamImplementation<TPart>(
|
||||
|
||||
Reference in New Issue
Block a user