React

The default builder, the store hooks, and swapping in your own components.

pnpm add @qnot/core @qnot/react

The default builder

<QueryBuilder> takes either a store you created, or store options (fields, initial or value/onChange, format, settings, sanitize) and creates one.

import '@qnot/core/styles.css';
import { QueryBuilder } from '@qnot/react';

<QueryBuilder fields={fields} initial={saved} onChange={save} showLock />;

Owning the store

useQueryStore(options) creates a store for the component's lifetime. Options are read once, except value (synced into the store), onChange (always the latest) and settings (applied when the object changes). Share it with <QueryProvider>:

import { QueryBuilder, QueryProvider, useHistory, useIsValid, useQueryStore } from '@qnot/react';

function Editor() {
  const store = useQueryStore({ fields, initial: saved, settings: { maxRules: 50 } });
  return (
    <QueryProvider store={store}>
      <Toolbar />
      <QueryBuilder store={store} />
    </QueryProvider>
  );
}

function Toolbar() {
  const { canUndo, canRedo, undo, redo } = useHistory();
  const valid = useIsValid();
  return (/* … */);
}

Hooks

All hooks read the store from the nearest <QueryProvider>. Pass your schema as a type parameter for typed results.

useRule actions: setField, setOperator, setValue, setRef, setCall, setLocked, remove (asks confirmRemove first) and clone. useGroup actions: setCombinator, setNot, setMatch, setLocked, addRule, addGroup, addArrayGroup, remove, clone.

can holds booleans for what the user may do now (editField, editOperator, editValue, remove, clone, lock for rules; addRule, addGroup, editGroup, remove, clone, lock for groups). Disable controls with it; the store refuses the same edits anyway.

Writing your own UI

A custom builder is a group component that renders its children by id, and a rule component:

import { QueryProvider, useGroup, useNode, useRule } from '@qnot/react';
import { memo } from 'react';

const Group = memo(function Group({ id }: { id: string }) {
  const { group, addRule, setCombinator } = useGroup(id);
  return (
    <fieldset>
      <select value={group.combinator} onChange={(e) => setCombinator(e.target.value as 'and' | 'or')}>
        <option value="and">all</option>
        <option value="or">any</option>
      </select>
      {group.children.map((child) => <Node id={child} key={child} />)}
      <button onClick={() => addRule()}>Add rule</button>
    </fieldset>
  );
});

function Node({ id }: { id: string }) {
  return useNode(id)?.kind === 'group' ? <Group id={id} /> : <Rule id={id} />;
}

const Rule = memo(function Rule({ id }: { id: string }) {
  const { rule, fieldOptions, operators, editor, setField, setOperator, setValue, remove } = useRule(id);
  /* render rule.field, rule.operator, and an editor for editor.type */
});

Wrap each piece in memo and pass only the id: the per-node subscriptions do the rest, and editing one rule re-renders only that rule. The custom UI guide builds a complete one with coss ui.

Drag and drop

Three hooks wire HTML5 drag and drop to the store's move. They work in the default builder and in your own:

import { useDragHandle, useDropTarget, useGroupDropZone } from '@qnot/react';

function Row({ id }: { id: string }) {
  const { isDragging, handleProps } = useDragHandle(id);
  const { position, dropProps } = useDropTarget(id); // position: 'before' | 'after' | null
  return (
    <div {...dropProps} data-dragging={isDragging} data-drop={position}>
      <span {...handleProps}>⠿</span>
      {/* … */}
    </div>
  );
}

useGroupDropZone(groupId) makes a group's children area accept drops at its end, including when it's empty. Moves respect canReorder, canRegroup, maxDepth and locks.