@cgarciagarcia/react-query-builder

中文文档
1.22.3发布于 2026/10/26MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 4506

创建兼容 spartie/ laravel- query- buildinger 的查询字符串

中文使用文档
文档目录29 节
  1. 目录
  2. 安装
  3. 快速启动
  4. 配置
  5. API 参考
  6. 过滤器
  7. 字段
  8. 排序
  9. 包括
  10. 参数
  11. 倾斜
  12. 公用事业
  13. 高级: 冲突过滤器
  14. 从 URL 水分
  15. 30秒的例子
  16. 只读水合( 无 URL 写入)
  17. 自定义写入器
  18. 与外部变化相对应( builder.rehydrate() (中文(简体) )
  19. 异名: 保留您的前端名称, 船后端名称
  20. URL 和后端的不同名称
  21. 从 URL 中隐藏噪音状态 : urlOmit
  22. 重新命名 URL 密钥
  23. 锁定来自 URL 的密钥
  24. 想要另一个来源吗? 写自己的适配器
  25. 下层
  26. 已知的限制
  27. 支助
  28. 考虑支助
  29. 许可证

@cgarciagarcia/反应-query-建设者

TypeScript 反应钩钩,用于构建兼容的查询字符串 温泉/泻水-清水-建材。 。 。 .

Coverage Status Test CI License: MIT Codacy Badge Downloads


目录


安装

# npm
npm install @cgarciagarcia/react-query-builder

# yarn
yarn add @cgarciagarcia/react-query-builder

# pnpm
pnpm add @cgarciagarcia/react-query-builder

同伴依赖性 : 反应17,18,或19.


快速启动

import { useQueryBuilder } from "@cgarciagarcia/react-query-builder";

const builder = useQueryBuilder()

builder
  .fields('user.name', 'user.last_name')
  .filter('age', 18)
  .filter('salary', '>', 1000)
  .sort('created_at')
  .sort('age', 'desc')
  .include('posts', 'comments')
  .setParam('external_param', 123)
  .page(1)
  .limit(10)

// Use in fetch
fetch("https://myapi.com/api/users" + builder.build())

// builder.build() returns:
// ?fields[user]=name,last_name&filter[age]=18&filter[salary][gt]=1000&sort=created_at,-age&includes=posts,comments&external_param=123&page=1&limit=10

配置

传递一个可选配置对象到 useQueryBuilder 设置初始状态并自定义行为.

const builder = useQueryBuilder({
  // Map frontend field names to backend names
  aliases: {
    "frontend_name": "backend_name",
  },

  // Pre-set initial state
  filters: [],
  includes: [],
  sorts: [],
  fields: [],
  params: {},

  // Define mutually exclusive filters (see Advanced section)
  pruneConflictingFilters: {},

  // Custom delimiters (default: ',')
  delimiters: {
    global: ',',    // applies to all unless overridden
    fields: null,
    filters: null,
    sorts: null,
    includes: null,
    params: null,
  },

  // Prepend '?' to the output of build()
  useQuestionMark: false,

  // Initial pagination state
  pagination: {
    page: 1,
    limit: 10,
  },
})

API 参考

所有方法都返回构建者实例,所以它们都是 可连锁。 。 。 .

过滤器

// Add a filter (appends values by default)
builder.filter('status', 'active')

// Add a filter with an operator
builder.filter('salary', '>', 1000)
builder.filter('age', '>=', 18)

// Override existing filter values instead of appending
builder.filter('status', 'inactive', true)

// Remove specific filters
builder.removeFilter('status', 'age')

// Remove all filters
builder.clearFilters()

// Check if filters exist
builder.hasFilter('status')           // → boolean
builder.hasFilter('status', 'age')    // → true only if ALL exist

可用的运算符 : =, (中文(简体) ), <, (中文(简体) ), >, (中文(简体) ), <=, (中文(简体) ), >=, (中文(简体) ), <>

您也可以导入 FilterOperator 用于类型安全操作器:

import { FilterOperator } from "@cgarciagarcia/react-query-builder"

builder.filter('salary', FilterOperator.GreaterThan, 1000)

字段

builder.fields('name', 'email', 'user.avatar')
builder.removeField('email')
builder.clearFields()
builder.hasField('name')   // → boolean

排序

builder.sort('created_at')           // default: asc
builder.sort('age', 'desc')
builder.removeSort('created_at', 'age')
builder.clearSorts()
builder.hasSort('created_at')        // → boolean

包括

builder.include('posts', 'comments')
builder.removeInclude('posts')
builder.clearIncludes()
builder.hasInclude('posts')          // → boolean

参数

builder.setParam('custom_key', 'value')
builder.setParam('ids', [1, 2, 3])
builder.removeParam('custom_key')
builder.clearParams()
builder.hasParam('custom_key')       // → boolean

倾斜

const builder = useQueryBuilder({
  pagination: { page: 1, limit: 10 }
})

builder.page(3)           // go to page 3
builder.nextPage()        // page + 1
builder.previousPage()    // page - 1 (stops at 1)
builder.limit(25)         // change page size

builder.getCurrentPage()  // → number | undefined
builder.getLimit()        // → number | undefined

说明: 更改过滤器,去掉过滤器,或将限制自动重排到第1页.


公用事业

build()

返回最后的查询字符串.

builder.build() // → "?filter[age]=18&sort=created_at"

toArray()

返回作为平面字符串数组的查询状态。 作为反应查询有用 queryKey。 。 。 .

import { useQuery } from "@tanstack/react-query"

const builder = useQueryBuilder()

const { data } = useQuery({
  queryFn: () => getUsers(builder.build()),
  queryKey: ['users', ...builder.toArray()],
})

tap(callback)

检查内部状态而不中断链.

builder
  .filter('age', 18)
  .tap((state) => console.log(state))
  .sort('name')

when(condition, callback)

有条件执行基于布尔的回调 。 建造者无论怎样都会返回.

builder.when(isAdmin, (state) => {
  console.log('Admin state:', state)
})

高级: 冲突过滤器

一些过滤器在后端是相互排斥的(例如. date 对 between_dates) (中文(简体) ). 使用 pruneConflictingFilters 让库自动处理.

const builder = useQueryBuilder({
  pruneConflictingFilters: {
    date: ['between_dates'],
    // 'between_dates': ['date'] is added automatically (bidirectional)
  },
})

builder.filter('date', '2024-08-13')
// → ?filter[date]=2024-08-13

builder.filter('between_dates', ['2024-08-06', '2024-08-13'])
// → ?filter[between_dates]=2024-08-06,2024-08-13
// (date filter was automatically removed)

冲突是 默认双向 - 你只需要声明一次 如果您愿意,也可以明确声明双向.


从 URL 水分

说你有一个列表视图 有过滤器,种类和相片。 用户选择"status = active",按日期排序,打开第3页——并与队友共享链接. 您希望链接能用已经应用的完全相同的过滤器打开 .

这就是一个 适配器 是否:它将建造者连接到一个外部来源(URL, localStorage随便你 内置 createSearchParamsAdapter 处理 URL 大小写.

30秒的例子

import {
  useQueryBuilder,
  createSearchParamsAdapter,
} from "@cgarciagarcia/react-query-builder";

const builder = useQueryBuilder({
  adapter: createSearchParamsAdapter({ sync: true }),
});

builder.filter("status", "active").sort("created_at", "desc");
// URL bar is now: /?filter[status]=active&sort=-created_at

刷新页面,共享链接——过滤器返回. 就是这样.

刚才发生的事:

  • 在挂载时,适配器 读取 当前 URL 和种子构建者.
  • sync: true 接下来 挂载后写一次 以最终状态实现 URL 正常化( 所以) BaseConfig 默认, urlOmit,或者一个僵化的链接,用户在即时得到调节——不需要突变),以及随后的每一个变化.
  • URL 输出映射什么 .build() 生产,所以你的后端和URL栏都同意.

只读水合( 无 URL 写入)

如果您只想在挂载时水分, 且之后再也不能触碰 URL, 请离开 sync 输出 :

useQueryBuilder({
  adapter: createSearchParamsAdapter(),
});

现在 read() 当钩子挂起时运行一次——与 useState(() => …) - 事情就这样结束了 挂载后的 URL 变化不会重新水合 .

自定义写入器

sync 接受三种表格,取决于您希望URL更新有多积极:

// 1) Default behavior: replaceState (no extra browser history entries)
createSearchParamsAdapter({ sync: true });           // alias for "replace"

// 2) pushState (every mutation is a back-button step)
createSearchParamsAdapter({ sync: "push" });

// 3) Bring your own — useful for Next.js / React Router / debouncing
createSearchParamsAdapter({
  sync: (search) => router.replace({ search }),
});

挂载时常态化 运行在全部三种模式中——在构建者构建之后,刻出一次,所以URL栏立即反映最终状态. 对于 sync: "push" 第一次通话仍然使用 replaceState (所以上山起火不会增加"幽灵回扣"条目;您的自定义回扣也会在上山上起火一次,如果这让你的路由器感到惊讶,则用关闭旗来守卫.

不要担心其他应用程序的查询参数——编剧 保存着它不承认的任何东西。 。 ?utm_source=newsletter 或者说 ?theme=dark 当过滤器被添加、更新或清除时,保持不动.

与外部变化相对应(builder.rehydrate()(中文(简体) )

read() 运行 一次 在山上。 后出为建州主. 如果有别的东西改变源头——用户点击回/Forward,另一个标签会变异 localStorage,一个websocket推出一个新的服务器侧过滤器——建设者自己不会注意到.

builder.rehydrate() 重新运行适配器 read() ,并将数据层(filters / skinds / 包括 / 字段 /params / pagination)替换为来源现在所说的任何内容。 配置(别名、划界人、, pruneConflictingFilters, (中文(简体) ), useQuestionMark)被保留.

const builder = useQueryBuilder({
  adapter: createSearchParamsAdapter({ sync: true }),
});

// Re-pull state from the URL after the browser navigates.
useEffect(() => {
  const onPop = () => builder.rehydrate();
  window.addEventListener("popstate", onPop);
  return () => window.removeEventListener("popstate", onPop);
}, [builder]);

为什么手动而不是自动? 不同来源需要不同的听众(popstate 为历史, storage 用于跨tab localStorage,用于服务器状态的 Websocket订阅,...). 连接右听器属于你的代码,其中的选择是显而易见的. 图书馆别挡道.

异名: 保留您的前端名称, 船后端名称

你可能给UI起个名字userName, (中文(简体) ), createdAt)和 API 中的另一个name, (中文(简体) ), created_at) (中文(简体) ). 过 aliases 到构建器,适配器处理翻译 两个方向 自动 :

useQueryBuilder({
  aliases: { userName: "name", createdAt: "created_at" },
  adapter: createSearchParamsAdapter({ sync: true }),
});

// Your code keeps using frontend names:
builder.filter("userName", "Jane").sort("createdAt", "desc");

// The URL bar shows backend names (same as .build() would emit):
// /?filter[name]=Jane&sort=-created_at

// On refresh, ?filter[name]=Jane hydrates back as { userName: "Jane" }.

适配器会自动从构建器配置读取别名,所以你只声明一次.

URL 和后端的不同名称

URL和后端不必共享相同的命名. 也许你希望URL能说用户的语言(?filter[documento]=…)而您的 API 保留技术名称( )?filter[code]=…) (中文(简体) ). 或者你的代码 rol 西班牙文,但URL应改为: type 用于共享链接.

过 urlAliases 切换到适配器 aliases数字 :

useQueryBuilder({
  aliases:    { dni: "code", rol: "type" },        // state → backend (drives .build())
  adapter: createSearchParamsAdapter({
    sync: true,
    urlAliases: { dni: "documento", rol: "type" }, // state → URL bar
  }),
});

builder.filter("dni", "12345").filter("rol", "admin");
// URL bar:  ?filter[documento]=12345&filter[type]=admin   ← user sees this
// .build(): ?filter[code]=12345&filter[type]=admin        ← API receives this

相同逻辑属性的三个独立命名空间:

层名称由
状态( 您的代码)dni, (中文(简体) ), rol你怎么称呼 .filter()
URL 栏documento, (中文(简体) ), typeadapter.urlAliases
后端code, (中文(简体) ), typebuilder.aliases

奥米 urlAliases 以让 URL 共享构建者的名称(普通例)。 过 urlAliases: {} 明确选择退出任何 URL 翻译 —— 然后 URL 逐字映射您的状态空间名称, 即使当 builder.aliases 已经设定.

安全说明:使用 URL QQ后端,知道后端名的攻击者可以进行手动操作 ?filter[code]=… 直接说 用适配器的许可列表将其锁定 :

adapter: createSearchParamsAdapter({
  sync: true,
  urlAliases: { dni: "documento", rol: "type" },
  allowed: { filters: ["documento", "type"] },   // backend-name attempts → dropped
}),

从 URL 中隐藏噪音状态 : urlOmit

有时建设者会携带您的条目 后端需要, 但用户不应该在 URL 栏中看到。典型案例:默认包含/字段 API 总是需要 (include=organization,permissions那只是繁杂的共享链接.

urlOmit 每个桶只写一个拒绝列表。 所列名字仍留在州内(因此 .build() 他们只是没有到达 URL:

useQueryBuilder({
  includes: ["organization", "permissions"],     // always sent to API
  adapter: createSearchParamsAdapter({
    sync: true,
    urlOmit: { includes: ["organization", "permissions"] },
  }),
});

builder.filter("status", "active");
// .build() → ?filter[status]=active&include=organization,permissions   ← backend
// URL bar  → ?filter[status]=active                                    ← user

通关卡 "*" 将桶中的每个条目都放下——用于 fields,这是用户在URL中很少需要看到的内部API优化:

adapter: createSearchParamsAdapter({
  sync: true,
  urlOmit: {
    fields: ["*"],                                // hide ALL fields from URL
    includes: ["organization", "permissions"],    // hide only these
  },
}),

IDE 自动完成建议 "*" 感谢一个字形的 工会打字技巧, 所以它是可以发现的 同时仍然接受任何 简单的属性名称.

异形觉悟于 filters 财务报告和审定财务报表 sorts ——在词汇表(状态或URL)中列出一个名称,两个表单都跳过.

说明数字 : urlOmit 只影响作家。 如果一个精心设计的URL包含其中一个名称,读者仍然会处理. 将相同名称添加到 excludeKeys 如果你也想在水合时拒绝的话.

重新命名 URL 密钥

有时默认的 URL 是动词 :

?filter[status]=active&filter[role]=admin&sort=-created_at&include=author,tags&fields[user]=id,name

这是一个口活——足够长的时间在聊天预览中溢出,丑陋的分享,更难扫描. 重新绘制密钥以缩短 :

createSearchParamsAdapter({
  keys: { filter: "filt", sort: "srt", include: "inc", fields: "fld" },
  sync: true,
});

现在,同样的状态产生:

?filt[status]=active&filt[role]=admin&srt=-created_at&inc=author,tags&fld[user]=id,name

为什么你可能想要这个:

  • 更短,更可分享的链接 ——每个参数可保存字节,当您拥有许多过滤器时,差异很大.
  • 清理 URL 栏 ——方便用户和截取支持票的截图.
  • 品牌或域语言 - 你的应用程序说"克瑞",不是"过滤器"? 匹配用户已经知道的词汇表 .
  • 避免相撞 ——如果周边应用已经使用 ?filter=... 或者说 ?sort=... 重命名来共存.

读者和作家都使用新密钥,所以来回转接仍然有效. 这个 仅限 影响 URL 栏.. .build() (你传递给 fetch继续用克南克语 filter编号 :sort/... 命名您的后端预期 .

锁定来自 URL 的密钥

URL是用户输入. 没有限制,一个精心设计的链接像 ?filter[is_admin]=true 会直接流入你的 .build() 呼叫和出到你的后端。 你有两个原始人, 每个桶你选一个符合你情况的人

allowed ——严格许可列表("只有这些")

使用它,当你有一个简短的,明确的清单 合法的东西:

createSearchParamsAdapter({
  allowed: {
    filters: ["status", "role", "created_at"],
    sorts:   ["created_at", "name"],
    params:  ["locale", "tenant"],   // anything not here is dropped
  },
});

省略桶时的默认值 :

  • filters 编号 : sorts 编号 : includes 编号 : fields ~ 允许一切( 这样 URL 可以自由驱动过滤)
  • params - 不允许任何东西(包罗万象是逐一否认的——参数是任意的,所以你必须选择加入,列出它们)

excludeKeys ——定点拒绝名单("除这些之外的一切")

使用它时,你一般相信桶 但想屏蔽一些危险的名字—— 不列举一切合法:

createSearchParamsAdapter({
  // No allowed.filters → I accept any filter from the URL.
  // I have 47 legitimate filter attributes and I add more every month;
  // maintaining a full whitelist is brittle. But is_admin and password
  // must NEVER come through the URL.
  excludeKeys: {
    filters: ["is_admin", "password"],
  },
});

我该用哪一个?

情况使用
"Short,明确列出什么是允许的"allowed
"接受一切,除了这些"excludeKeys
"白名单加上顶上移动的拒绝名单"两者(防御深度)

如果你准备好了 allowed.filters: ["status", "role"],列表 is_admin 内 excludeKeys.filters 实值 冗余 ——已经因为不允许而放弃了. 它不会打破任何东西(并且读作意向文件),但不会在运行时拉起重量. 当您保持一个稳定的允许列表并想要一个单独的,快速变化的拒绝列表时,该组合是有用的.

适用于两者的细节

  • 两者均适用于 读者和作家 对称性地——URL栏总是在您的政策之内.

  • excludeKeys 总是胜出 allowed (低拍允许).

  • 异形觉悟于 filters 财务报告和审定财务报表 sorts。 。 。 . 当你准备好 aliases,如果 无论是 uRL 边名(后端) 或 状态边名(前端) 在列表中 。 以您所想的词汇书写您的规则 :

    aliases: { userName: "name" },
    allowed: { filters: ["userName"] }   // ✓ allows ?filter[name]=...
    // — or —
    allowed: { filters: ["name"] }       // ✓ also allows it
    

    否认的一方 excludeKeys: { filters: ["is_admin"] } 两个块 ?filter[is_admin]=... 和 ?filter[adminFlag]=... (别名键),因此攻击者不能通过切换词汇来绕过拒绝列表.

  • 对于 fields,可以通过短道具匹配(password)或通过 entity.prop (单位:千美元)user.password) (中文(简体) ). 选择你需要的精确度.

  • page 财务报告和审定财务报表 limit 为: 自动水合 当在 URL 中出现,并在 设置中由 作者 释放 state.pagination. 圆通出箱-无额外配置.

  • 为什么每桶? password 是个危险的过滤器 但很好 fields 选择( 它只是选择 API 返回的哪一列) 。 一个单子会迫使你进入全局或一无所有.

想要另一个来源吗? 写自己的适配器

QueryBuilderAdapter 刚刚 { read, write? }。包罗一切:

// Persist to localStorage instead of the URL
const localStorageAdapter: QueryBuilderAdapter = {
  read:  () => JSON.parse(localStorage.getItem("filters") ?? "{}"),
  write: (state) => localStorage.setItem("filters", JSON.stringify(state)),
};

useQueryBuilder({ adapter: localStorageAdapter });

相同的图案为 react-router 搜索参数,散列路由器,内存商店,索引DB – 您可以从中读取并写入任何内容 .

下层

如果您只想要没有钩子集成的 URL 解析器, 则两块都会导出为纯函数 :

import {
  parseSearchParams,
  serializeSearchParams,
} from "@cgarciagarcia/react-query-builder";

// URL → state
parseSearchParams("?filter[status]=active&sort=-name");
// → { filters: [...], sorts: [{ attribute: "name", direction: "desc" }] }

// state → URL
serializeSearchParams({
  filters: [{ attribute: "status", value: ["active"] }],
});
// → "filter[status]=active"

都接受同样的 keys 编号 : urlAliases 编号 : allowed 编号 : excludeKeys 选项作为适配器.

已知的限制

URL协议使用几个字符作为控制符号. 包含它们的过滤值 沉默地堕落 在来回旅行中——写作者释放出这个值,但读者却将它分成不同的. 在我们解除这个限制之前 把这些当作过滤器内限值:

字符为什么它坏了
,多值分隔器 - filter[tag]=a,b 变成 ["a", "b"] 解析
<, (中文(简体) ), >, (中文(简体) ), <=, (中文(简体) ), >=, (中文(简体) ), <> (作为 领导 前缀)运算符语法 - filter[age]=>=18 成为运算符 >= + 数值 ["18"]

其他"特长"人物如: %, (中文(简体) ), &, (中文(简体) ), + 和空间是好的——它们游走 URL-逃逸 和在环行无恙.

如果您需要这些字符中的任何一个( 如带有逗号的标记), 请在 URL 适配器外保留该部分状态, 或者在传递到您侧面之前避开它 .filter()。 。 。 .


支助

有问题或需要帮助吗? 打开讨论 (原始内容存档于2018-09-29). on GitHub.


考虑支助

如果此软件包帮助您, 请考虑支持其创建者 :

付费页 : @carlosgarciadev : 翻译:


许可证

麻省理工学院许可证(MIT). 见 自由主义 更多信息.

版本与发布记录

@cgarciagarcia/react-query-builder 共发布 97 个版本,首次发布于 2024/07/15;过去 12 个月发布了 21 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.22.3latest2026/10/2614 天前
1.22.22026/10/2515 天前
1.22.12026/10/2416 天前
1.22.02026/05/225 个月前
1.21.42026/03/297 个月前
1.21.32026/03/297 个月前

关于 @cgarciagarcia/react-query-builder 的常见问题

@cgarciagarcia/react-query-builder 是什么?

创建兼容 spartie/ laravel- query- buildinger 的查询字符串

如何安装 @cgarciagarcia/react-query-builder?

在项目目录运行 npm install @cgarciagarcia/react-query-builder,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^17.0.0 || ^18.0.0 || ^19.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cgarciagarcia/react-query-builder 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cgarciagarcia/react-query-builder 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cgarciagarcia/react-query-builder 可以商用吗?

@cgarciagarcia/react-query-builder 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cgarciagarcia/react-query-builder 的下载量有多少?

根据 npm 官方下载统计,@cgarciagarcia/react-query-builder 在 2026/10/10 – 2026/11/08 这 30 天内被下载 4,506 次,日均约 150 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cgarciagarcia/react-query-builder 还在维护吗?

最新版本 1.22.3 发布于 2026/10/26(14 天前);至今共发布 97 个版本,其中过去 12 个月发布了 21 个;首次发布于 2024/07/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。