Skip to content

fix(ui): form state race conditions #12026

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Apr 10, 2025
Merged

Conversation

jacobsfletch
Copy link
Member

@jacobsfletch jacobsfletch commented Apr 7, 2025

Fixes form state race conditions. Modifying state while a request is in flight or while the response is being processed could result in those changes being overridden.

This was happening for a few reasons:

  1. Our merge logic was incorrect. We were disregarding local changes to state that may have occurred while form state requests are pending. This was because we were iterating over local state, then while building up new state, we were ignoring any fields that did not exist in the server response, like this:

    for (const [path, newFieldState] of Object.entries(existingState)) {
    
      if (!incomingState[path]) {
        continue
      }
      
      // ...
    }

    To fix this, we need to use local state as the source of truth. Then when the server state arrives, we need to iterate over that. If a field matches in local state, merge in any new properties. This will ensure all changes to the underlying state are preserved, including any potential addition or deletions.

    However, this logic breaks down if the server might have created new fields, like when populating array rows. This means they, too, would be ignored. To get around this, there is a new addedByServer property that flags new fields to ensure they are kept.

    This new merge strategy also saves an additional loop over form state.

  2. We were merging form state based on a mutable ref. This meant that changes made within one action cause concurrent actions to have dirty reads. The fix for this is to merge in an isolated manner by copying state. This will remove any object references. It is generally not good practice to mutate state without setting it, anyways, as this causes mismatches between what is rendered and what is in memory.

  3. We were merging server form state directly within an effect, then replacing state entirely. This meant that if another action took place at the exact moment in time after merge but before dispatch, the results of that other action would be completely overridden. The fix for this is to perform the merge within the reducer itself. This will ensure that we are working with a trustworthy snapshot of state at the exact moment in time that the action was invoked, and that React can properly queue the event within its lifecycle.

@jacobsfletch jacobsfletch marked this pull request as ready for review April 10, 2025 14:51
@jmikrut jmikrut merged commit 4d7c1d4 into main Apr 10, 2025
78 checks passed
@jmikrut jmikrut deleted the fix/form-state-race-conditions branch April 10, 2025 16:11
Copy link
Contributor

🚀 This is included in version v3.34.0

@jacobsfletch jacobsfletch linked an issue Apr 17, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Out of sync UI and client form state in payload blocks
2 participants