Summary
FilteringState.setArtifact builds the next artifact record from the render-snapshot artifacts closure, so two setArtifact calls batched into one React commit each spread the same stale record and the second write silently erases the first. There is also no bulk replace (setAllArtifacts), so restoring a saved filter combination (filter presets) cannot be expressed at all without one-write-per-commit gymnastics.
Environment
Reproduction
const { filtering } = useDataViewContext()
// apply a stored two-filter "preset" in one tick
await act(async () => {
filtering.setArtifact('title', { mode: 'contains', query: 'alpha' })
filtering.setArtifact('status', { values: ['published'] })
})
filtering.getArtifact('status') // { values: ['published'] } ✓
filtering.getArtifact('title') // { mode: 'contains', query: '' } ✗ — write lost
Expected behavior
Both artifacts survive — setArtifact on two different filter names must compose, the same way two successive useState functional updates compose. Alternatively (and ideally in addition), FilteringState exposes a bulk setAllArtifacts(record: Record<string, FilterArtifact>) so a stored filter combination can be applied atomically.
Actual behavior
Only the last setArtifact of the commit survives; every earlier one comes back as the default artifact. No error is thrown — the writes are silently lost.
Suspected root cause
packages/bindx-dataview/src/useDataViewState.ts (0.1.46, lines 76–94): setArtifact / resetFilter precompute the next record from the render-snapshot artifacts and pass it to setArtifacts as a plain value:
const setArtifact = useCallback(
(name: string, artifact: FilterArtifact): void => {
setArtifacts({ ...artifacts, [name]: artifact })
},
[artifacts, setArtifacts],
)
useStoredState's setter does use the functional setState form internally, but the update it receives is already a stale-based value, so batched calls each spread the same artifacts and the last one wins.
Suggested fix
In useFilteringState:
- Make
setArtifact / resetFilter use the functional update form: setArtifacts(current => ({ ...current, [name]: artifact })) — useStoredState's SetStoredState already supports it.
- Add a bulk
setAllArtifacts(artifacts: Record<string, FilterArtifact>): void to FilteringState (a whole-record replace, setArtifacts(next)), which is the natural primitive for applying a saved preset and mirrors what resetAll already does with defaultArtifacts.
useSortingState.setOrderBy has the same stale-snapshot pattern (lines 184–206) and would benefit from the same treatment, plus a setDirections(directions: SortingDirections) for restoring persisted sorting.
Workaround shipped downstream
We applied a temporary workaround in our project, marked TODO [BindX] (<this-issue-url>): <description>. The workaround seeds the grid's StateStorage backend with the preset's artifact record and remounts the DataGrid via a React key (state storage is read in the useState initializer, so a remount restores the full record atomically); we will remove it once this issue is resolved.
Summary
FilteringState.setArtifactbuilds the next artifact record from the render-snapshotartifactsclosure, so twosetArtifactcalls batched into one React commit each spread the same stale record and the second write silently erases the first. There is also no bulk replace (setAllArtifacts), so restoring a saved filter combination (filter presets) cannot be expressed at all without one-write-per-commit gymnastics.Environment
@contember/bindx@0.1.46(version installed in the reporting project)contember/bindx@mainas of3c2fd0dtests/react/dataview/filteringSetArtifactBatchClobber.test.tsxbug/filtering-set-artifact-batch-clobberReproduction
Expected behavior
Both artifacts survive —
setArtifacton two different filter names must compose, the same way two successiveuseStatefunctional updates compose. Alternatively (and ideally in addition),FilteringStateexposes a bulksetAllArtifacts(record: Record<string, FilterArtifact>)so a stored filter combination can be applied atomically.Actual behavior
Only the last
setArtifactof the commit survives; every earlier one comes back as the default artifact. No error is thrown — the writes are silently lost.Suspected root cause
packages/bindx-dataview/src/useDataViewState.ts(0.1.46, lines 76–94):setArtifact/resetFilterprecompute the next record from the render-snapshotartifactsand pass it tosetArtifactsas a plain value:useStoredState's setter does use the functionalsetStateform internally, but the update it receives is already a stale-based value, so batched calls each spread the sameartifactsand the last one wins.Suggested fix
In
useFilteringState:setArtifact/resetFilteruse the functional update form:setArtifacts(current => ({ ...current, [name]: artifact }))—useStoredState'sSetStoredStatealready supports it.setAllArtifacts(artifacts: Record<string, FilterArtifact>): voidtoFilteringState(a whole-record replace,setArtifacts(next)), which is the natural primitive for applying a saved preset and mirrors whatresetAllalready does withdefaultArtifacts.useSortingState.setOrderByhas the same stale-snapshot pattern (lines 184–206) and would benefit from the same treatment, plus asetDirections(directions: SortingDirections)for restoring persisted sorting.Workaround shipped downstream
We applied a temporary workaround in our project, marked
TODO [BindX] (<this-issue-url>): <description>. The workaround seeds the grid'sStateStoragebackend with the preset's artifact record and remounts theDataGridvia a Reactkey(state storage is read in theuseStateinitializer, so a remount restores the full record atomically); we will remove it once this issue is resolved.