Summary
In StimulusReflex 3.5.x, ReflexData#params merges params in the wrong order, causing URL query-string params (from the original page load) to override serialized form data (what the user just submitted). This silently discards user input for any field whose name also appears in the page URL.
Root cause
lib/stimulus_reflex/reflex_data.rb:
def params
form_params.deep_merge(url_params) # url_params win — WRONG
end
deep_merge gives precedence to the argument, so url_params (stale, from the initial GET request) overwrite form_params (current user input). The correct order is:
def params
url_params.deep_merge(form_params) # form_params win — CORRECT
end
How to reproduce
- Load a page via GET with array params in the URL, e.g.
?items[1][tags][]= (empty — this is the hidden Rails input that ensures the field is always submitted even when nothing is selected).
- The user selects a value in a
<select multiple> on the form.
- A reflex button with
data-reflex-serialize-form="true" fires.
- In the reflex,
params[:items]["1"][:tags] is [""] (from the URL) instead of the user's selection.
Impact
- Any reflex that reads
params for fields that appear in the page URL will see the original page-load value, not the user's current input.
- Multi-selects and checkboxes are especially affected because Rails always emits a hidden
field[]= with an empty value, which ends up in the URL and then stomps the real selection on the next reflex call.
- The bug is invisible unless server-side code inspects the affected field — making it very easy to miss in testing.
Migration context
In SR 3.4.x, combined dataset mode automatically serialized the form and the result was merged as form_data.deep_merge(data["params"]) — no URL params were involved at all. When upgrading to 3.5.x and replacing combined with ancestors + data-reflex-serialize-form="true", the new URL-param inclusion silently breaks the contract that existed in 3.4.x.
Workaround (until fixed)
Monkey-patch ReflexData in an initializer:
module StimulusReflexFormParamsFix
def params
url_params.deep_merge(form_params)
end
end
StimulusReflex::ReflexData.prepend(StimulusReflexFormParamsFix)
Environment
- stimulus_reflex gem: 3.5.5
- stimulus_reflex npm: 3.5.5
- Rails: 8.0
- Ruby: 3.4.x
Summary
In StimulusReflex 3.5.x,
ReflexData#paramsmerges params in the wrong order, causing URL query-string params (from the original page load) to override serialized form data (what the user just submitted). This silently discards user input for any field whose name also appears in the page URL.Root cause
lib/stimulus_reflex/reflex_data.rb:deep_mergegives precedence to the argument, sourl_params(stale, from the initial GET request) overwriteform_params(current user input). The correct order is:How to reproduce
?items[1][tags][]=(empty — this is the hidden Rails input that ensures the field is always submitted even when nothing is selected).<select multiple>on the form.data-reflex-serialize-form="true"fires.params[:items]["1"][:tags]is[""](from the URL) instead of the user's selection.Impact
paramsfor fields that appear in the page URL will see the original page-load value, not the user's current input.field[]=with an empty value, which ends up in the URL and then stomps the real selection on the next reflex call.Migration context
In SR 3.4.x,
combineddataset mode automatically serialized the form and the result was merged asform_data.deep_merge(data["params"])— no URL params were involved at all. When upgrading to 3.5.x and replacingcombinedwithancestors+data-reflex-serialize-form="true", the new URL-param inclusion silently breaks the contract that existed in 3.4.x.Workaround (until fixed)
Monkey-patch
ReflexDatain an initializer:Environment