Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Removed

- **Breaking:** `createRuleContext` and `createRuleGroupContext` are no longer exported. Each was a
single-line `createMemo` around `QueryManager#getRuleContext` / `#getRuleGroupContext` keyed on
query identity, with no consumer inside the package. Callers should read the manager directly —
`createMemo(() => (query(), manager.getRuleContext(path())))` is the whole implementation.

### Changed

- Internal: ~20 `createMemo` calls that wrapped a single property read or a primitive-returning
boolean expression are now plain closures. Solid props are already lazy getters, so those memos
allocated a computation node to cache a property access. Memos that allocate an object, run a
user callback (`validator`, `accessibleDescriptionGenerator`), or exist for identity stability
(`disabledPaths`, the `configVersion`-keyed option lists) are untouched. `createRuleState`'s
`resolvers` memo is inlined into the `ctx` memo it exclusively fed.
- `createRuleState` and `createRuleGroupState` take a plain `RuleProps` / `RuleGroupProps` instead
of `Props | Accessor<Props>`. A getter-object literal is equally reactive, which is what the one
internal accessor caller (`RuleSubQuery`) was already passing.

## [0.1.0] - 2026-08-08

### Changed
Expand Down
174 changes: 88 additions & 86 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions examples/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"check": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@react-querybuilder/core": "^8.22.4",
"@solidjs/web": "2.0.0-beta.32",
"solid-js": "2.0.0-beta.32"
"@react-querybuilder/core": "^8.22.5",
"@solidjs/web": "2.0.0-rc.0",
"solid-js": "2.0.0-rc.0"
},
"devDependencies": {
"typescript": "^5.9.3",
Expand Down
6 changes: 3 additions & 3 deletions examples/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"test:ssr": "bun run ./ssr-smoke-test.ts"
},
"dependencies": {
"@react-querybuilder/core": "^8.22.4",
"@solidjs/web": "2.0.0-beta.32",
"solid-js": "2.0.0-beta.32",
"@react-querybuilder/core": "^8.22.5",
"@solidjs/web": "2.0.0-rc.0",
"solid-js": "2.0.0-rc.0",
"solid-querybuilder": "workspace:*"
},
"devDependencies": {
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.5",
"@solidjs/testing-library": "1.0.0-beta.2",
"@solidjs/web": "2.0.0-beta.32",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/user-event": "^14.6.3",
"@solidjs/web": "2.0.0-rc.0",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/user-event": "^14.6.4",
"@types/bun": "^1.3.14",
"@types/node": "^26.1.2",
"@vitest/coverage-v8": "^4.1.10",
"jsdom": "^30.0.1",
"oxfmt": "^0.62.0",
"oxlint": "^1.77.0",
"oxfmt": "^0.63.0",
"oxlint": "^1.78.0",
"sass": "^1.102.0",
"solid-js": "2.0.0-beta.32",
"solid-js": "2.0.0-rc.0",
"typescript": "^5.9.3",
"vite": "^8.2.0",
"vite-plugin-solid": "3.0.0-next.23",
Expand Down
10 changes: 5 additions & 5 deletions packages/solid-querybuilder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,16 @@
"conformance:test": "vitest run --config vitest.conformance.config.ts"
},
"peerDependencies": {
"solid-js": "^2.0.0-beta.32",
"@solidjs/web": "^2.0.0-beta.32"
"solid-js": "^2.0.0-rc.0",
"@solidjs/web": "^2.0.0-rc.0"
},
"dependencies": {
"@react-querybuilder/core": "^8.22.4"
"@react-querybuilder/core": "^8.22.5"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.5",
"@solidjs/web": "2.0.0-beta.32",
"solid-js": "2.0.0-beta.32",
"@solidjs/web": "2.0.0-rc.0",
"solid-js": "2.0.0-rc.0",
"typescript": "^5.9.3",
"vite": "^8.2.0",
"vite-plugin-solid": "3.0.0-next.23"
Expand Down
4 changes: 2 additions & 2 deletions packages/solid-querybuilder/scripts/check-versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ const read = async (pkg: string): Promise<string | undefined> => {

// Exact pins. `solid-js` and `@solidjs/web` must move together, and only deliberately.
const exact: Record<string, string> = {
'solid-js': '2.0.0-beta.32',
'@solidjs/web': '2.0.0-beta.32',
'solid-js': '2.0.0-rc.0',
'@solidjs/web': '2.0.0-rc.0',
Comment thread
jakeboone02 marked this conversation as resolved.
'vite-plugin-solid': '3.0.0-next.23',
'@solidjs/testing-library': '1.0.0-beta.2',
};
Expand Down
4 changes: 3 additions & 1 deletion packages/solid-querybuilder/src/internal/RuleSubQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ export const RuleSubQuery = (props: { ruleProps: RuleProps; parts: RuleState }):
},
} as unknown as RuleGroupProps;

const groupState = createRuleGroupState(() => groupProps);
// `groupProps` is a getter-object literal with a fixed identity, so passing it directly is
// exactly as reactive as wrapping it in an accessor was.
const groupState = createRuleGroupState(groupProps);

return (
<RuleComponents ruleProps={p()} parts={state()} subQuery={{ groupProps, parts: groupState }} />
Expand Down
29 changes: 16 additions & 13 deletions packages/solid-querybuilder/src/reactive/createQueryBuilderState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
QueryManagerOptions,
RuleGroupTypeAny,
RuleType,
ValidationMap,
ValueEditorType,
ValueSourceFullOptions,
} from '@react-querybuilder/core';
Expand Down Expand Up @@ -199,9 +200,11 @@ export const createQueryBuilderState = <
| QueryManager<RuleGroupTypeAny, F, FullOperator, FullCombinator>
| undefined;

const maxLevels = createMemo(() =>
(getProps().maxLevels ?? 0) > 0 ? Number(getProps().maxLevels) : Infinity
);
// A plain closure: it returns a primitive, so there is no identity to stabilize.
const maxLevels = (): number =>
(getProps().maxLevels ?? 0) > 0 ? Number(getProps().maxLevels) : Infinity;
// Memoized, unlike `maxLevels`: it returns an array, and `structuralOptions` compares it by
// value. Identity stability is the point here, not the cost of the derivation.
const disabledPaths = createMemo(() =>
Array.isArray(getProps().disabled) ? (getProps().disabled as Path[]) : emptyDisabledPaths
);
Expand Down Expand Up @@ -521,20 +524,21 @@ export const createQueryBuilderState = <
const actions = createRuleActions<F, O>(getProps, manager);

// #region Derived config
const independentCombinators = createMemo(() => isRuleGroupTypeIC(query()));
const queryDisabled = createMemo(() => getProps().disabled === true);
const rootGroupDisabled = createMemo(
() => !!query().disabled || disabledPaths().some(p => p.length === 0)
);
// Plain closures: each returns a primitive over reads that allocate nothing.
const independentCombinators = (): boolean => isRuleGroupTypeIC(query());
const queryDisabled = (): boolean => getProps().disabled === true;
const rootGroupDisabled = (): boolean =>
!!query().disabled || disabledPaths().some(p => p.length === 0);

const validationResult = createMemo(() => {
const { validator } = getProps();
return typeof validator === 'function' ? validator(query()) : emptyValidationMap;
});
const validationMap = createMemo(() => {
// `validationResult` stays memoized — it runs a user callback. This only reshapes its result.
const validationMap = (): ValidationMap => {
const result = validationResult();
return typeof result === 'boolean' ? emptyValidationMap : result;
});
};

// A disabled root *group* does not disable the wrapper, so this reads `queryDisabled` rather
// than `rootGroupDisabled`.
Expand All @@ -547,9 +551,8 @@ export const createQueryBuilderState = <
})
);

const inlineCombinatorsAttr = createMemo(() =>
independentCombinators() || config().showCombinatorsBetweenRules ? 'enabled' : 'disabled'
);
const inlineCombinatorsAttr = (): string =>
independentCombinators() || config().showCombinatorsBetweenRules ? 'enabled' : 'disabled';
// #endregion

// A getter object, not a memo returning a fresh object: a Solid context value is read once by
Expand Down
105 changes: 0 additions & 105 deletions packages/solid-querybuilder/src/reactive/createRuleContext.test.ts

This file was deleted.

40 changes: 0 additions & 40 deletions packages/solid-querybuilder/src/reactive/createRuleContext.ts

This file was deleted.

32 changes: 0 additions & 32 deletions packages/solid-querybuilder/src/reactive/createRuleGroupContext.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { RuleGroupType, RuleGroupTypeAny, RuleGroupTypeIC } from '@react-querybuilder/core';
import { createSignal, flush } from 'solid-js';
import { describe, expect, it } from 'vitest';
import { setupInRoot } from '../../test/reactive-harness.js';
import {
Expand Down Expand Up @@ -229,25 +228,6 @@ describe('createRuleGroupState', () => {
});

describe('reactivity', () => {
it('accepts an accessor and tracks it', () => {
const { actions } = createRecordingActions();
const state = setupInRoot(() =>
createQueryBuilderState(baseProps({ defaultQuery: flatQuery }))
);
const [not, setNot] = createSignal(false);
const result = setupInRoot(() =>
createRuleGroupState(() =>
ruleGroupProps(state, { ...flatQuery, not: not() } as RuleGroupType, { actions })
)
);

expect(result.ruleGroup.not).toBe(false);
setNot(true);
// Reads lag writes; without the flush this assertion would see the old value.
flush();
expect(result.ruleGroup.not).toBe(true);
});

it('returns getters, not accessors, so members read without a call', () => {
const { result } = setup();
const descriptor = Object.getOwnPropertyDescriptor(result, 'outerClassName');
Expand Down
Loading