Skip to main content
Version: Next

Custom components with fallbacks

Sometimes a default component nearly meets your needs, but requires minor behavioral modifications. Rather than recreating the entire component, you can implement custom behavior and fall back to the default component when appropriate. This approach keeps your implementation current with standard features while maintaining customization flexibility.

Consider a scenario where you need a custom date picker for specific fields, while using the standard value editor for others. The default ValueEditor lacks date picker functionality, requiring a custom solution.

Instead of duplicating the default ValueEditor code, you can leverage its existing functionality by spreading the same props (<ValueEditor {...props} />) and returning it when your custom behavior doesn't apply.

This example creates a custom value editor using the react-datepicker library. We'll start by configuring the fields array with standard Field objects, adding a custom datatype attribute to date fields that signals when to display the date picker.

// fields.ts
import { Field } from 'react-querybuilder';

export const fields: Field[] = [
{
name: 'name',
label: 'Name',
operators: [
{ name: '=', label: 'is' },
{ name: 'beginsWith', label: 'begins with' },
],
},
{
name: 'dateOfBirth',
label: 'Date of Birth',
operators: [{ name: '=', label: 'is' }],
datatype: 'date',
},
{
name: 'dateRange',
label: 'Date Range',
operators: [{ name: 'between', label: 'is between' }],
datatype: 'dateRange',
},
];

The custom value editor displays different interfaces based on the field's datatype:

  • "date": Standard date picker
  • "dateRange": Date range picker
  • Other values or undefined: Falls back to the default ValueEditor

We use the date-fns library for date parsing and formatting. Storing dates as strings (rather than Date objects) keeps the query object serializable for JSON.stringify. Date ranges are stored as comma-separated string pairs.

// CustomValueEditor.tsx
import { format, parse } from 'date-fns';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { ValueEditor, ValueEditorProps } from 'react-querybuilder';

const dateFormat = 'yyyy-MM-dd';

export const CustomValueEditor = (props: ValueEditorProps) => {
if (props.fieldData.datatype === 'date') {
return (
<div>
<DatePicker
dateFormat={dateFormat}
selected={!props.value ? null : parse(props.value, dateFormat, new Date())}
onChange={(d: Date) => props.handleOnChange(d ? format(d, dateFormat) : null)}
/>
</div>
);
} else if (props.fieldData.datatype === 'dateRange') {
const [startDate, endDate] = props.value.split(',');
return (
<div>
<DatePicker
selectsRange
dateFormat={dateFormat}
startDate={!startDate ? null : parse(startDate, dateFormat, new Date())}
endDate={!endDate ? null : parse(endDate, dateFormat, new Date())}
onChange={(update: [Date, Date]) => {
const [s, e] = update;
props.handleOnChange(
[!s ? '' : format(s, dateFormat), !e ? '' : format(e, dateFormat)].join(',')
);
}}
/>
</div>
);
}
return <ValueEditor {...props} />;
};
tip

If you're using one of the compatibility packages, you probably want to fall back to the value editor from that package instead of ValueEditor from the main package. For example, when using @react-querybuilder/antd, fall back to AntDValueEditor:

-import { ValueEditor, ValueEditorProps } from 'react-querybuilder';
+import { AntDValueEditor } from '@react-querybuilder/antd';
+import { ValueEditorProps } from 'react-querybuilder';
-  return <ValueEditor {...props} />;
+ return <AntDValueEditor {...props} />;

Configure the QueryBuilder component to use the custom value editor through the controlElements prop:

// App.tsx
import { useState } from 'react';
import { CustomValueEditor } from './CustomValueEditor';
import { fields } from './fields';

export default function App() {
const [query, setQuery] = useState({ combinator: 'and', rules: [] });
return (
<QueryBuilder
fields={fields}
query={query}
onQueryChange={setQuery}
controlElements={{ valueEditor: CustomValueEditor }}
/>
);
}

The interactive demo below shows how each field type behaves: the "Name" field displays a text input, "Date of Birth" shows a standard date picker, and "Date Range" presents a date range picker.

import { format, parse } from 'date-fns';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { ValueEditor, ValueEditorProps } from 'react-querybuilder';

const dateFormat = 'yyyy-MM-dd';

export const CustomValueEditor = (props: ValueEditorProps) => {
  if (props.fieldData.datatype === 'date') {
    return (
      <div>
        <DatePicker
          dateFormat={dateFormat}
          selected={!props.value ? null : parse(props.value, dateFormat, new Date())}
          onChange={(d: Date) => props.handleOnChange(d ? format(d, dateFormat) : null)}
        />
      </div>
    );
  } else if (props.fieldData.datatype === 'dateRange') {
    const [startDate, endDate] = props.value.split(',');
    return (
      <div>
        <DatePicker
          selectsRange
          dateFormat={dateFormat}
          startDate={!startDate ? null : parse(startDate, dateFormat, new Date())}
          endDate={!endDate ? null : parse(endDate, dateFormat, new Date())}
          onChange={(range: [Date, Date]) => {
            const [s, e] = range;
            props.handleOnChange(
              [!s ? '' : format(s, dateFormat), !e ? '' : format(e, dateFormat)].join(',')
            );
          }}
        />
      </div>
    );
  }
  return <ValueEditor {...props} />;
};

note

Other examples of the "fallback" technique can be seen in the Limit rule groups page and these two StackOverflow answers.