Skip to content

trackQuery.entered re-fires when an unrelated query key changes #94

Description

@sergeysova

Package

@effector/router (core)

What happened?

trackQuery's entered event (and $state) re-fires on every $query update while the tracker is eligible (its routes match the active route) — even when none of the schema-owned keys actually changed. A chain sampled off tracker.entered therefore re-runs whenever an unrelated query parameter changes.

Root cause

In packages/core/lib/track-query.ts:

const $result = combine($eligible, $query, (eligible, query) =>
  eligible ? parameters.safeParse(query) : null,
);

safeParse returns a new object reference on every call, so combine treats $result (and the derived $evaluation) as changed on any $query update, regardless of whether the schema-relevant fields differ from the previous parse. The downstream sample that fires entered has no equality guard, so it re-emits with logically identical parsed params.

Reproduction

const home = createRoute({ path: '/' });
const controls = createRouterControls();
const router = createRouter({ routes: [home], controls });
const scope = fork();
await allSettled(router.setHistory, { scope, params: historyAdapter(createMemoryHistory({ initialEntries: ['/'] })) });

const tracker = trackQuery({
  controls,
  routes: [home],
  parameters: z.object({ id: z.string() }),
});
const enteredCalls = watchCalls(tracker.entered, scope);

await allSettled(router.navigate, { scope, params: { path: '/', query: { id: '1' } } });
// enteredCalls: 1

await allSettled(router.navigate, { scope, params: { path: '/', query: { id: '1', tab: 'details' } } });
// enteredCalls: 2 (unchanged id, unrelated key added)

await allSettled(router.navigate, { scope, params: { path: '/', query: { id: '1', tab: 'other' } } });
// enteredCalls: 3 (unrelated key changed again)

Expected: enteredCalls stays at 1 across both extra navigations, since id never changes.
Actual: fires again on every unrelated query mutation.

Suggested direction

Add an equality guard on the parsed result (e.g. updateFilter/deep-equal on result.data, or compare only schema-owned keys) before deriving entered/$state, so unrelated query keys don't cause spurious re-entry.

Traceability

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions