Skip to main content

【Development Journey in Rybit】 How Changing an Asset Quota Was Quietly Overloading Our Server

Background

This is one of the fixes I implemented in Rybit company and I want to share the story behind it. It’s a great example of how a small UI change can have hidden performance implications, and how understanding React's rendering behavior is key to optimizing user experience.

Edit Assets Drawer: Adjusting asset quota and site details

In Rybit's B2B order management, the Edit Assets Drawer is one of the most frequently used workflows — operators use it to adjust how many assets a Partner needs for the next subscription period and which site each asset should transfer to.

Network panel: 3 legitimate triggers vs 1 unnecessary trigger on quota adjust

I discovered that every time the operator adjusted the "Number of assets for next period" field, the system sent a POST API with all asset IDs to fetch each asset's current site — even if the operator didn't change any asset IDs. This was an unnecessary API call that could overload our server and degrade user experience. Even when server response was fast, the redundant API call caused a noticeable delay in the UI, making the application feel sluggish.

Problem

Keep all 3 legitimate API triggers: open drawer, remove asset, edit asset ID

These three actions require fetching asset site data, we want to keep those API calls:

  1. When the drawer opens, the system fetches each asset's current site to populate the "Site" column.

  2. When the operator removes an asset, the system refetches sites for the remaining assets.

  3. When the operator edits an asset ID and blurs the input, the system fetches the site for the new asset ID.

Unnecessary API trigger: adjusting quota number without changing asset IDs

  1. When the operator adjusts the "Number of assets for next period" field, the system sends the same POST API call again — even if the operator doesn't change any asset IDs. This is unnecessary and should be eliminated.
ActionExpected to trigger api?
Open the drawer✓ Keep
Remove an asset from the table✓ Keep
Edit an asset ID (onBlur)✓ Keep
Adjust "Number of assets for next period"✗ Unnecessary — payload is identical, asset list unchanged

Root Cause: Unstable initialValues Causes Child Remount

Original data flow:

Original data flow: state-driven initialValues causes Form to remount children on quota change

When the user adjusts the quota, AssetQuotaArea called updateEditAssetsDrawerFormValues, writing the new value back into Detail.tsx state. State update → new initialValues object reference → Antd Form treats any new initialValues reference as a reset and remounts all child components.

adjust quota → state update → initialValues reference changes → Form remounts children → useEffectOnce fires → API call

useEffectOnce is designed to fire only once per component lifetime — but the guarantee is per mount, not per lifetime. Each remount creates a fresh component instance, so the effect fires again as if it had never run.

Original code causing the redundant API call

original-code-1
original-code-2original-code-3

Fix

Overview

Improved flow: adjusting quota updates form state directly, no remounts, no unnecessary API calls

1. Stabilize initialValues with useMemo

Replace state-driven initialValues with a const computed from server data. Reference only changes when the dependency (order data) changes, so the Form doesn't remount on quota adjustments:

Stabilized initialValues: useMemo computes a stable reference that only changes when order data changes

2. Move quota updates inside the Form: useWatch + setFieldValue

Convert AssetQuotaArea to a controlled component under <FormItem>. Remove updateEditAssetsDrawerFormValues. Detect quota changes with useWatch, update disabled state with setFieldValue:

Move quota state inside Form: useWatch detects changes, setFieldValue updates dependent field state, no external state or remounts needed

3. Detect the formNextAssetQuota value with useWatch

const formNextAssetQuota = useWatch<number>(editAssetsDrawerFormKeys.nextAssetQuota, form) ?? 0;

useWatch subscribes to formNextAssetQuota, so when the operator adjusts the quota, we can conditionally disable or enable the site inputs without causing any remounts or API calls.

Disable site input when quota is zero: useWatch observes quota changes and conditionally disables site inputs to prevent invalid edits

4. Sync EditableProTable's editableKeys

After adjusting Number of assets for next period, update new datasoure and set new newEditableKeys for EditableProTable:

Sync editableKeys with dataSource: ensure input display state matches the current data, especially after edits or deletions


Result

ActionBeforeAfter
Open the drawerFires ✓Fires ✓
Remove an assetFires ✓Fires ✓
Edit an asset ID (onBlur)Fires ✓Fires ✓
Adjust the quota numberFires (unnecessary)Does not fire ✓

Key Takeaways

  1. Antd Form initialValues is for initialization only. Any reference change triggers child remounts — don't use it as a live state channel.
  2. For cross-field interactions inside a Form, keep state inside the Form. Use useWatch to observe and setFieldValue to update, rather than lifting to external useState and feeding back through initialValues.
  3. EditableProTable's editableKeys must stay in sync with datasource. It controls the input display state of each row independently from the data itself.