Skip to content

setArtifact clobbers sibling writes within one commit; no bulk setAllArtifacts for restoring saved filter state #66

Description

@vparys

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:

  1. Make setArtifact / resetFilter use the functional update form: setArtifacts(current => ({ ...current, [name]: artifact }))useStoredState's SetStoredState already supports it.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions