Quick start
A typed query builder in React, from schema to SQL.
Define your fields
The schema is the one place you describe what can be queried. Everything else, from the UI's editors to the types of rule values and the SQL, follows from it.
import { , } from '@qnot/core';
export const = ({
: .(),
: .({ : 0, : true }),
: .(['free', 'pro', 'team']),
: .(),
: .({ : 'Signed up' }),
});Render a builder
<QueryBuilder> creates a store from your fields and renders the default, unstyled UI.
import '@qnot/core/styles.css';
import { QueryBuilder } from '@qnot/react';
import type { QueryOf } from '@qnot/core';
import { fields } from './fields';
export function Filters({ onChange }: { onChange: (q: QueryOf<typeof fields>) => void }) {
return <QueryBuilder fields={fields} onChange={onChange} />;
}Use the query
The value is qnot's own JSON: a tree of groups and rules, typed by your schema.
const : <typeof > = {
: 'group',
: 'and',
: [
{ : 'rule', : 'age', : 'gte', : 21 },
{ : 'rule', : 'plan', : 'in', : ['pro', 'team'] },
],
};Turn it into SQL, or filter rows directly:
import { compile, encode } from '@qnot/core';
import { sqlFormat } from '@qnot/core/formats/sql';
const { sql, params } = encode(sqlFormat({ dialect: 'postgres', params: 'numbered' }), query, fields);
// sql: "age >= $1 AND plan IN ($2, $3)", params: [21, 'pro', 'team']
const adults = users.filter(compile(fields, query));Store-first or controlled
<QueryBuilder> accepts store options directly, which is enough for most forms. When other parts of
the page need the query (a preview, a results table, undo buttons), create the store yourself and
share it:
import { QueryBuilder, QueryProvider, useHistory, useQuery, useQueryStore } from '@qnot/react';
function Page() {
const store = useQueryStore({ fields, initial: savedQuery });
return (
<QueryProvider store={store}>
<QueryBuilder store={store} />
<Preview />
</QueryProvider>
);
}
function Preview() {
const query = useQuery<typeof fields>(); // re-renders on every change
return <pre>{JSON.stringify(query, null, 2)}</pre>;
}It also works controlled, like an input. Passing back the value it gave you costs nothing, because the store recognises its own output and skips reconciling:
const [query, setQuery] = useState(initialQuery);
<QueryBuilder fields={fields} value={query} onChange={setQuery} />;