Skip to content

fix: provide default empty array for watched form fields#5196

Merged
perkinsjr merged 2 commits intomainfrom
seer/fix/rbac-watch-defaults
Mar 22, 2026
Merged

fix: provide default empty array for watched form fields#5196
perkinsjr merged 2 commits intomainfrom
seer/fix/rbac-watch-defaults

Conversation

@seer-by-sentry
Copy link
Copy Markdown
Contributor

@seer-by-sentry seer-by-sentry Bot commented Mar 4, 2026

What does this PR do?

This PR fixes an issue where watchedDirectPermissionIds was non-iterable because usePersistedForm loaded corrupted state, causing a spread operator failure.

To address this, an empty array [] was added as the default value for watch("roleIds") and watch("directPermissionIds") to ensure these fields are always arrays. This prevents potential issues when these form fields are uninitialized or have no selected values.

Fixes UNKEY-DASHBOARD-3Y

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

To test this, ensure that form fields roleIds and directPermissionIds are always treated as arrays, even when uninitialized or having no selected values. Specifically, verify that operations expecting iterable values on watchedDirectPermissionIds (and potentially watchedRoleIds) no longer fail due to non-iterable types. This can be reproduced by scenarios where usePersistedForm might load corrupted or unexpected state for these fields.

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Ran make fmt on /go directory
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

@seer-by-sentry seer-by-sentry Bot requested a review from perkinsjr March 4, 2026 18:08
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboard Ready Ready Preview, Comment Mar 22, 2026 4:18pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 27a08f5e-9f4c-40ab-b9e3-1dd45c040198

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A single file update to the KeyRbacDialog component adds default empty array values to watched form fields. This prevents undefined access errors when computing effective permissions and in dependent hooks.

Changes

Cohort / File(s) Summary
Edit RBAC Component
web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/index.tsx
Added default empty array values for watch("roleIds") and watch("directPermissionIds") to ensure watched values are always arrays, preventing undefined access in allEffectivePermissionIds computation and dependent hooks.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~2 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: providing default empty arrays for watched form fields in the RBAC component.
Description check ✅ Passed The description covers all required sections including what the PR does, type of change, testing instructions, and checklist items, though some checklist items remain unchecked.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch seer/fix/rbac-watch-defaults

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can get early access to new features in CodeRabbit.

Enable the early_access setting to enable early access features such as new models, tools, and more.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/index.tsx:
- Around line 106-107: The watched values returned by watch("roleIds", []) and
watch("directPermissionIds", []) can be non-array (null or other types) when
persisted state is corrupted, so normalize at read-time: read the raw value via
watch(...) into local variables (e.g., rawRoleIds, rawDirectPermissionIds) and
coerce to arrays using Array.isArray checks (e.g., const watchedRoleIds =
Array.isArray(rawRoleIds) ? rawRoleIds : []; similarly for
watchedDirectPermissionIds) and optionally map/validate element types if needed;
update any downstream code that uses .includes() or array spread to use these
normalized watchedRoleIds/watchedDirectPermissionIds so operations won’t throw
on malformed persisted state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 88872f19-2bc7-4bdd-8a87-006c4d2b30ef

📥 Commits

Reviewing files that changed from the base of the PR and between 97f4ad2 and 9060b8b.

📒 Files selected for processing (1)
  • web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/index.tsx

Comment on lines +106 to +107
const watchedRoleIds = watch("roleIds", []);
const watchedDirectPermissionIds = watch("directPermissionIds", []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

React Hook Form v7 watch method defaultValue parameter behavior when field value is null or invalid type

💡 Result:

In React Hook Form v7, watch(name, defaultValue) (and useWatch({ name, defaultValue })) uses defaultValue only as a fallback when the watched field is not yet set / returns undefined (commonly on the first render before the field is registered, or when a value doesn’t exist in form state). It’s not a general “nullish coalescing” fallback. [1] [2] [3]

So, for your cases:

  • If the field value is null: watch() returns null (because the field is set), and the defaultValue parameter is not applied. RHF generally expects you to use null or "" as the “cleared” value rather than undefined. [4]
  • If the field value is an “invalid type”: watch() returns whatever is currently stored in RHF’s form state—it does not coerce types based on defaultValue, and validation errors don’t change what watch() returns. (Type-safety is primarily a TypeScript concern at compile time, not runtime.) [1] [3]
  • If the field value is undefined: RHF treats that like “not set” and you’ll observe fallback behavior to defaults (either the watch/useWatch defaultValue, or useForm({ defaultValues })). Also, setting a field to undefined effectively can snap it back to its default. [1] [5]

Practical rule: if you need a fallback when a value is null, do it yourself: const v = watch('x'); const resolved = v ?? myFallback; (or handle null explicitly), because RHF’s defaultValue is primarily for the undefined/unregistered case. [1] [3]

Sources
[1] https://reacthookform.caitouyun.com/api/useform/watch
[2] https://react-form-hook.com/docs/usewatch
[3] https://app.unpkg.com/react-hook-form@7.70.0/files/dist/watch.d.ts
[4] https://react-form-hook.com/docs/usecontroller/controller
[5] https://github.com/orgs/react-hook-form/discussions/5858


🏁 Script executed:

head -150 "web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/index.tsx" | tail -100

Repository: unkeyed/unkey

Length of output: 2907


🏁 Script executed:

wc -l "web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/index.tsx"

Repository: unkeyed/unkey

Length of output: 214


🏁 Script executed:

rg "updateKeyRbacSchema" --type tsx --type ts -A 15 | head -50

Repository: unkeyed/unkey

Length of output: 84


🏁 Script executed:

rg "usePersistedForm" --type tsx --type ts -B 3 -A 10 | head -80

Repository: unkeyed/unkey

Length of output: 84


🏁 Script executed:

fd "edit-rbac" --type f

Repository: unkeyed/unkey

Length of output: 213


🏁 Script executed:

cat -n "web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/hooks/use-edit-rbac.ts"

Repository: unkeyed/unkey

Length of output: 3627


🏁 Script executed:

cat -n "web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/index.tsx" | head -150

Repository: unkeyed/unkey

Length of output: 5871


🏁 Script executed:

fd "update-key-rbac.schema" --type f

Repository: unkeyed/unkey

Length of output: 226


🏁 Script executed:

fd "use-persisted-form" --type f

Repository: unkeyed/unkey

Length of output: 153


🏁 Script executed:

cat -n "web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/update-key-rbac.schema.ts"

Repository: unkeyed/unkey

Length of output: 597


🏁 Script executed:

cat -n "web/apps/dashboard/hooks/use-persisted-form.tsx"

Repository: unkeyed/unkey

Length of output: 3627


Normalize array fields at read-time to handle corrupted persisted state

The watch(..., []) defaultValue only applies when a field is undefined. If persisted state contains malformed values (null, non-array types), watch() will return the corrupted value, and downstream operations (Line 113: .includes(), Line 116: spread) will fail or misbehave. Since usePersistedForm directly restores JSON without schema validation, add runtime normalization when accessing these watched values.

Normalize array fields
-  const watchedRoleIds = watch("roleIds", []);
-  const watchedDirectPermissionIds = watch("directPermissionIds", []);
+  const normalizeStringArray = (value: unknown): string[] =>
+    Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
+
+  const watchedRoleIds = normalizeStringArray(watch("roleIds"));
+  const watchedDirectPermissionIds = normalizeStringArray(watch("directPermissionIds"));

This aligns with the coding guideline to parse inputs at boundaries into typed structures, preventing illegal states from propagating through the component logic.

📝 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 watchedRoleIds = watch("roleIds", []);
const watchedDirectPermissionIds = watch("directPermissionIds", []);
const normalizeStringArray = (value: unknown): string[] =>
Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
const watchedRoleIds = normalizeStringArray(watch("roleIds"));
const watchedDirectPermissionIds = normalizeStringArray(watch("directPermissionIds"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/index.tsx
around lines 106 - 107, The watched values returned by watch("roleIds", []) and
watch("directPermissionIds", []) can be non-array (null or other types) when
persisted state is corrupted, so normalize at read-time: read the raw value via
watch(...) into local variables (e.g., rawRoleIds, rawDirectPermissionIds) and
coerce to arrays using Array.isArray checks (e.g., const watchedRoleIds =
Array.isArray(rawRoleIds) ? rawRoleIds : []; similarly for
watchedDirectPermissionIds) and optionally map/validate element types if needed;
update any downstream code that uses .includes() or array spread to use these
normalized watchedRoleIds/watchedDirectPermissionIds so operations won’t throw
on malformed persisted state.

@chronark chronark enabled auto-merge March 5, 2026 11:33
@perkinsjr perkinsjr disabled auto-merge March 22, 2026 16:27
@perkinsjr perkinsjr merged commit e6b42bf into main Mar 22, 2026
13 of 14 checks passed
@perkinsjr perkinsjr deleted the seer/fix/rbac-watch-defaults branch March 22, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants