Skip to content

Best Practices

This guide covers best practices and recommended patterns when building with Filter Sphere.

Organize your filter code into separate files by concern. This makes each piece easier to reuse, test, and maintain independently.

filter-sphere/
├── index.tsx # Main component, wires everything together
├── schema.ts # Zod data schema, default rules, filter functions
├── theme.tsx # Theme configuration via createFilterTheme
└── locale.ts # i18n setup with getLocaleText

Define your data with zod. Use .describe() on your Zod fields to provide human-readable labels or i18n keys. These labels are displayed in the field selector dropdown and can be translated via getLocaleText.

schema.ts
import { presetFilter } from "@fn-sphere/filter";
import { z } from "zod";
const schema = z.object({
name: z.string().describe("Name"),
email: z.string().describe("Email"),
age: z.number().describe("Age"),
active: z.boolean().describe("Active"),
createdAt: z.date().describe("Created At"),
});
// (Optional) Define a default filter rule
export const defaultRule = createFilterGroup({
op: "and",
conditions: [
createSingleFilter({
path: ["name"],
name: "contains",
args: ["default value"],
}),
],
});
// (Optional) Customize the list of filter functions
export const filterFnList: FnSchema[] = presetFilter
// Exclude "endsWith" as an example
.filter((fn) => fn.name !== "endsWith");

Use createFilterTheme to build your theme in a dedicated file. It merges your customizations with the preset defaults, so you only need to override what you want to change.

In most cases, overriding components is sufficient. The built-in components already handle event normalization internally (e.g., Input’s onChange provides a string directly instead of a raw ChangeEvent), so you rarely need to override primitives. Only override primitives when you need to swap the underlying HTML element itself, such as replacing native <button> with a UI library’s base element.

theme.tsx
import { createFilterTheme } from "@fn-sphere/filter";
export const theme = createFilterTheme({
components: {
// Override components — the recommended approach for most cases.
// Components provide a simplified API on top of primitives.
Button: (props) => <button {...props} className="my-button" />,
Input: ({ onChange, ...props }) => (
<input onChange={(e) => onChange?.(e.target.value)} {...props} />
),
},
templates: {
// Customize FilterGroupContainer, SingleFilter, etc.
},
// primitives: {
// // Only override primitives when you need to replace the base HTML elements.
// // e.g., swap native <button> with a UI library's base component.
// },
});

Keep i18n logic in a separate locale file. Use built-in locales from @fn-sphere/filter/locales as a base, and extend them with your own field name translations.

locale.ts
import { enUS, zhCN, jaJP } from "@fn-sphere/filter/locales";
export const locales = [
{ key: "en", label: "English", value: enUS },
{
key: "cn",
label: "中文",
value: { ...zhCN, Name: "名称", Email: "邮箱" },
},
{
key: "jp",
label: "日本語",
value: { ...jaJP, Name: "名前", Email: "メール" },
},
];
export const createGetLocaleText = (localeKey: string) => {
const locale = locales.find((l) => l.key === localeKey)?.value;
return (key: string): string => {
if (!locale || !(key in locale)) return key;
return locale[key as keyof typeof locale];
};
};

The field labels from .describe() are passed through getLocaleText, so you can translate them by adding the label as a key in your locale objects.

The main file imports from all other modules and composes them into the final component.

index.tsx
import {
FilterBuilder,
FilterSphereProvider,
useFilterSphere,
} from "@fn-sphere/filter";
import { defaultRule, filterFnList, filterSchema } from "./schema";
import { theme } from "./theme";
import { createGetLocaleText } from "./locale";
export function MyFilter() {
const { context, predicate, filterRule } = useFilterSphere({
schema: filterSchema,
defaultRule,
filterFnList,
getLocaleText: createGetLocaleText("en"),
});
const data = [
/* your data here */
];
const filteredData = data.filter(predicate);
console.log("Filtered Data:", filteredData);
return (
<FilterSphereProvider context={context} theme={theme}>
<FilterBuilder />
</FilterSphereProvider>
);
}