Description
The state.Provider's setData() method exhibits a counter-intuitive behavior when passed a nested object. A call like provider.setData({ user: { firstname: 'Jane' } }) currently replaces the entire user object, leading to the unintentional removal of other properties (e.g., lastname). This behavior is not only a potential source of bugs but also contradicts the hint in the official StateProviders.md guide, which suggests a merge operation occurs.
This ticket proposes a refactoring of the core setData logic to implement a more intuitive and powerful "drill-in and merge" paradigm. This will make the API more predictable, align it with the documentation, and enable the dynamic creation of new, fully reactive data structures at runtime.
Problem Statement
Counter-intuitive API: The current replacement behavior is unexpected. Developers naturally assume such an operation would perform a deep merge, updating firstname while preserving lastname.
Inconsistent Behavior: The behavior of setData({ user: {...} }) is inconsistent with setData({'user.firstname': ...}), where the latter correctly preserves sibling properties via "reactivity bubbling."
Incorrect Documentation: The guide in learn/guides/datahandling/StateProviders.md provides misleading information, creating a trap for developers.
Architectural Limitation: The inability to deep-merge makes it impossible to dynamically create new, fully reactive nested data structures at runtime (e.g., setData({ newConfig: { theme: 'dark' } })).
Proposed Solution
The internalSetData method in src/state/Provider.mjs will be refactored to implement the following logic:
Unconditional Drill-Down: When internalSetData receives a plain JavaScript object as a value (e.g., { firstname: 'Jane' } for the key 'user'), it will recursively call itself for each property within that object, creating the full path (e.g., 'user.firstname').
Atomic Records: This drill-down logic will explicitly check that the value is a plain object (Neo.typeOf(value) === 'Object'), ensuring that Neo.data.Record instances are correctly treated as atomic leaf values and are not drilled into.
Consistent Bubbling: After any leaf-node value is set (whether it's a primitive or a Record), the existing "reactivity bubbling" logic will execute. This ensures that parent objects are updated, triggering all relevant effects correctly and consistently.
This change ensures that all methods of setting data (proxy.prop = value, setData({prop: value}), setData({'prop.path': value})) converge on the same robust and predictable underlying behavior.
Acceptance Criteria
Code Implementation:
- The
internalSetData method in src/state/Provider.mjs is updated to reflect the "drill-in and merge" logic described above.
Testing:
- A new test case is added to
test/siesta/tests/state/ProviderNestedDataConfigs.mjs that explicitly asserts that provider.setData({ user: { firstname: 'Jane' } }) correctly merges the data and preserves user.lastname.
- All existing tests in
Provider.mjs and ProviderNestedDataConfigs.mjs must continue to pass.
Documentation:
- The incorrect hint in
learn/guides/datahandling/StateProviders.md must be removed. The section should be updated to accurately describe the deep-merge behavior.
- The code examples in the blog post
learn/blog/v10-deep-dive-state-provider.md must be updated to demonstrate and explain the correct, intuitive API usage.
Description
The
state.Provider'ssetData()method exhibits a counter-intuitive behavior when passed a nested object. A call likeprovider.setData({ user: { firstname: 'Jane' } })currently replaces the entireuserobject, leading to the unintentional removal of other properties (e.g.,lastname). This behavior is not only a potential source of bugs but also contradicts the hint in the officialStateProviders.mdguide, which suggests a merge operation occurs.This ticket proposes a refactoring of the core
setDatalogic to implement a more intuitive and powerful "drill-in and merge" paradigm. This will make the API more predictable, align it with the documentation, and enable the dynamic creation of new, fully reactive data structures at runtime.Problem Statement
Counter-intuitive API: The current replacement behavior is unexpected. Developers naturally assume such an operation would perform a deep merge, updating
firstnamewhile preservinglastname.Inconsistent Behavior: The behavior of
setData({ user: {...} })is inconsistent withsetData({'user.firstname': ...}), where the latter correctly preserves sibling properties via "reactivity bubbling."Incorrect Documentation: The guide in
learn/guides/datahandling/StateProviders.mdprovides misleading information, creating a trap for developers.Architectural Limitation: The inability to deep-merge makes it impossible to dynamically create new, fully reactive nested data structures at runtime (e.g.,
setData({ newConfig: { theme: 'dark' } })).Proposed Solution
The
internalSetDatamethod insrc/state/Provider.mjswill be refactored to implement the following logic:Unconditional Drill-Down: When
internalSetDatareceives a plain JavaScript object as a value (e.g.,{ firstname: 'Jane' }for the key'user'), it will recursively call itself for each property within that object, creating the full path (e.g.,'user.firstname').Atomic Records: This drill-down logic will explicitly check that the value is a plain object (
Neo.typeOf(value) === 'Object'), ensuring thatNeo.data.Recordinstances are correctly treated as atomic leaf values and are not drilled into.Consistent Bubbling: After any leaf-node value is set (whether it's a primitive or a
Record), the existing "reactivity bubbling" logic will execute. This ensures that parent objects are updated, triggering all relevant effects correctly and consistently.This change ensures that all methods of setting data (
proxy.prop = value,setData({prop: value}),setData({'prop.path': value})) converge on the same robust and predictable underlying behavior.Acceptance Criteria
Code Implementation:
internalSetDatamethod insrc/state/Provider.mjsis updated to reflect the "drill-in and merge" logic described above.Testing:
test/siesta/tests/state/ProviderNestedDataConfigs.mjsthat explicitly asserts thatprovider.setData({ user: { firstname: 'Jane' } })correctly merges the data and preservesuser.lastname.Provider.mjsandProviderNestedDataConfigs.mjsmust continue to pass.Documentation:
learn/guides/datahandling/StateProviders.mdmust be removed. The section should be updated to accurately describe the deep-merge behavior.learn/blog/v10-deep-dive-state-provider.mdmust be updated to demonstrate and explain the correct, intuitive API usage.