Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 37 additions & 45 deletions apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,13 @@ export function RuleForm({
watch,
setValue,
control,
formState: { errors, isSubmitting, isSubmitted },
formState ,
trigger,
} = form;

const { errors, isSubmitting, isSubmitted } = formState;


const {
fields: conditionFields,
append: appendCondition,
Expand Down Expand Up @@ -285,23 +288,6 @@ export function RuleForm({
) as CoreConditionType | undefined;
}, [conditions]);

// biome-ignore lint/correctness/useExhaustiveDependencies: needed
useEffect(() => {
trigger("conditions");
}, [conditions]);

const actionErrors = useMemo(() => {
const actionErrors: string[] = [];
watch("actions")?.forEach((_, index) => {
const actionError =
errors?.actions?.[index]?.url?.root?.message ||
errors?.actions?.[index]?.labelId?.root?.message ||
errors?.actions?.[index]?.to?.root?.message;
if (actionError) actionErrors.push(actionError);
});
return actionErrors;
}, [errors, watch]);

const conditionalOperator = watch("conditionalOperator");
const terminology = getEmailTerminology(provider);

Expand Down Expand Up @@ -351,18 +337,48 @@ export function RuleForm({
setIsNameEditMode((prev: boolean) => !prev);
}
}, [alwaysEditMode]);

function getErrorMessages(errors: FieldErrors<CreateRuleBody>): string[] {
const messages: string[] = [];

if (!errors) {
return messages;
}

if (typeof errors.name?.message === 'string') {
messages.push(errors.name.message);
}

const actionsMessage = errors.actions?.root?.message || errors.actions?.message;

if (typeof actionsMessage === 'string') {
messages.push(actionsMessage);
}

const conditionsMessage = errors.conditions?.root?.message || errors.conditions?.message;

if (typeof conditionsMessage === 'string') {
messages.push(conditionsMessage);
}

return messages;
}

const errorMessages = useMemo(() => {
return getErrorMessages(errors);
}, [formState]);
Comment on lines +367 to +369
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix useMemo dependency to match actual usage.

The useMemo depends on formState but getErrorMessages uses errors. Since errors is destructured from formState (line 165), the dependency array should reference errors directly to accurately track what the memoized function actually uses.

Apply this diff:

   const errorMessages = useMemo(() => {
     return getErrorMessages(errors);
-  }, [formState]);
+  }, [errors]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const errorMessages = useMemo(() => {
return getErrorMessages(errors);
}, [formState]);
const errorMessages = useMemo(() => {
return getErrorMessages(errors);
}, [errors]);
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx around lines 367
to 369, the useMemo currently lists formState as its dependency even though the
memoized callback only uses errors; update the dependency array to use errors
directly (i.e., replace formState with errors) so the memo updates correctly
when errors change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the GitHub issue to understand the context
gh issue view 3455 --repo react-hook-form/react-hook-form --json title,body,comments --jq '{title: .title, body: .body, comments: .comments[0:3]}'

Length of output: 4046


@ppranay20 Thank you for the correction! You're absolutely right.

After reading the issue, I now understand that react-hook-form uses a Proxy to track formState access, and the errors object is intentionally mutable. As the maintainer explained, using errors directly in dependency arrays won't trigger updates correctly because the same object reference is mutated in place.

Your current code using [formState] as the dependency is correct. This ensures the memo updates whenever any part of the form state changes, which is how react-hook-form is designed to work - it batches all formState updates together.

My apologies for the confusion!


🧬 Code Graph Analysis Results
  • File: apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx

    • Summary: getErrorMessages function
      • Description: Accepts an errors object produced by react-hook-form (or Yup/Zod resolver). It collects human-readable error messages from:
        • errors.name?.message
        • errors.actions?.root?.message or errors.actions?.message
        • errors.conditions?.root?.message or errors.conditions?.message
      • Return: string[] containing the collected error messages. This is used to render a consolidated error alert when the form is submitted with validation errors.
  • File: apps/web/utils/template.ts

    • Snippet:
      • hasVariables:
        export const hasVariables = (text: string | undefined | null) =>
        text ? new RegExp(TEMPLATE_VARIABLE_PATTERN).test(text) : false;
      • TEMPLATE_VARIABLE_PATTERN:
        export const TEMPLATE_VARIABLE_PATTERN = "\{\{[\s\S]*?\}\}";
    • Relevance: Used to detect if a string contains template variables (e.g., {{name}}). Relevant to understanding how the form handles variables within action/condition fields, which ties into the user comment about React Hook Form behavior and error handling.
  • File: apps/web/utils/actions/rule.validation.ts

    • Summary: CreateRuleBody type
      • Description: Exports CreateRuleBody as the inferred type from createRuleBody (Zod schema). This defines the shape of the rule data used by the form, including fields like id, name, runOnThreads, actions, conditions, conditionalOperator, and systemType.
      • Return: Type alias CreateRuleBody representing the validated rule payload structure.
    • Relevance: Helps in understanding the expected data shape that RuleForm.tsx validates and submits. Useful when diagnosing form schema-related issues (e.g., issues discussed in react-hook-form issue 3455).
  • File: apps/web/app/(app)/[emailAccountId]/assistant/ConditionSummaryCard.tsx (relevant context)

    • Summary: Example of how conditions are summarized in UI
      • Description: Maps condition types (AI, STATIC) to human-friendly summaries and icons. Demonstrates how the form’s conditions data is interpreted for display, which can help understand how error messages or validation messages relate to specific condition types in the form.
    • Relevance: Provides contextual understanding of how condition data is structured and presented, aiding debugging of condition-related form errors.
  • File: apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx (relevant context)

    • Summary: Example of how actions are summarized in UI
      • Description: Renders human-friendly summaries for various ActionType values (LABEL, DRAFT_EMAIL, REPLY, SEND_EMAIL, etc.) and shows how delay information is displayed. This helps correlate form data structure with UI rendering, which can be pertinent when interpreting validation errors tied to specific actions.
    • Relevance: Context for interpreting how form data maps to UI, relevant for diagnosing errors reported by the form when editing complex nested fields.

✏️ Learnings added
Learnt from: ppranay20
Repo: elie222/inbox-zero PR: 935
File: apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx:367-369
Timestamp: 2025-11-09T19:50:05.275Z
Learning: In React Hook Form, when using `useMemo` or `useEffect` with form errors, always use `formState` as the dependency rather than `errors` or `formState.errors`. This is because React Hook Form uses a Proxy pattern where the errors object is mutable and won't trigger dependency arrays correctly. The formState dependency ensures proper reactivity when form validation state changes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-07-18T15:04:57.115Z
Learning: Applies to **/*.tsx : Show validation errors inline next to form fields

Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-07-18T15:05:16.146Z
Learning: Applies to apps/web/components/**/*Form.tsx : Use `result?.serverError` with `toastError` and `toastSuccess` for error and success notifications in form submission handlers.

Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-07-20T09:03:06.318Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Make sure to pass a message value when creating a built-in error.

Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-07-18T15:04:57.115Z
Learning: Applies to **/*.tsx : Validate form inputs before submission

Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-07-20T09:03:06.318Z
Learning: Applies to **/*.{jsx,tsx} : Make sure all dependencies are correctly specified in React hooks.

Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-07-18T15:04:30.467Z
Learning: Applies to apps/web/components/**/*.tsx : Use React Hook Form with Zod validation for form handling

Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-07-18T15:05:16.146Z
Learning: Applies to apps/web/components/**/*Form.tsx : Use React Hook Form with Zod resolver for form handling and validation.

Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-07-18T15:04:57.115Z
Learning: Applies to **/*.tsx : Use React Hook Form with Zod for validation


return (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{isSubmitted && Object.keys(errors).length > 0 && (
{isSubmitted && errorMessages.length > 0 && (
<div className="mt-4">
<AlertError
title="Error"
description={
<ul className="list-disc">
{Object.values(errors).map((error) => (
<li key={error.message}>{error.message}</li>
{errorMessages.map((message, index) => (
<li key={index}>{message}</li>
))}
</ul>
}
Expand Down Expand Up @@ -461,15 +477,6 @@ export function RuleForm({
</div>
</div>

{errors.conditions?.root?.message && (
<div className="mt-2">
<AlertError
title="Error"
description={errors.conditions.root.message}
/>
</div>
)}

<div className="mt-2">
{conditionFields.map((condition, index) => (
<div key={condition.id}>
Expand Down Expand Up @@ -696,21 +703,6 @@ export function RuleForm({
)}
</div>

{actionErrors.length > 0 && (
<div className="mt-2">
<AlertError
title="Error"
description={
<ul className="list-inside list-disc">
{actionErrors.map((error, index) => (
<li key={`action-${index}`}>{error}</li>
))}
</ul>
}
/>
</div>
)}

<div className="mt-2 space-y-4">
{watch("actions")?.map((action, i) =>
isActionsEditMode ? (
Expand Down