Skip to content

Instantly share code, notes, and snippets.

@yarinsa
Created March 23, 2026 13:50
Show Gist options
  • Select an option

  • Save yarinsa/8471b94196ac06d27c686af08d054725 to your computer and use it in GitHub Desktop.

Select an option

Save yarinsa/8471b94196ac06d27c686af08d054725 to your computer and use it in GitHub Desktop.
Bidirectional Filter Architecture in React: Prior Art & References — Research into declarative URL↔GraphQL filter patterns

Bidirectional Filter Architecture in React: Prior Art & References

Research into existing patterns, libraries, and articles related to building a declarative, bidirectional filter system that maps between URL search params and structured query objects (e.g., GraphQL filters) in React/TypeScript.

The Pattern Under Investigation

A filter architecture with three co-located concerns per page:

URL Search Params  <-->  Filter Context (urlValues)  <-->  GraphQL Filter Object

Key characteristics:

  • Fluent builder API for declaring filters: .label().options().field().resolve().build()
  • Bidirectional mapping per filter via .field() (UI value → query object) and .resolve() (query object → UI value)
  • URL as source of truth using nuqs for typed URL state management
  • React Context bridge exposing toGraphQLFilter() and fromGraphQLFilter()
  • Filter reuse/extension pattern where base configs are extended per-page with UI overrides

Verdict

This pattern is novel. No existing library implements the full composition. The closest references cover individual layers but none combine all three: fluent builder DSL + bidirectional URL↔query mapping + React Context bridge.


Tier 1: Highly Relevant References

1. Harness Engineering — "From Messy to Modular: Rebuilding Filters in React"

  • URL: https://www.harness.io/blog/powering-harness-executions-page-inside-our-flexible-filters-component
  • Why it matters: The closest real-world architectural match. Implements:
    • useFiltersContext() backed by React Context
    • Centralized FiltersMap state object with typed values alongside serialized query strings
    • Full bidirectional sync: user interactions serialize to URL, URL changes parse back to typed state
    • Inversion of Control architecture — framework handles when/how state updates, parents define what happens
    • A parser interface for converting between typed values and URL-safe strings
  • Gap: No fluent builder API. No explicit per-filter write/read functions — uses a more imperative approach.

2. openstatusHQ/data-table-filters — Declarative Schema on nuqs

3. Hook Builder Gist — Type-Safe Query Params with Fluent API

  • Gist: https://gist.github.com/donaldpipowitch/95350eb1db4ac6fbecaff0b3790c712b
  • Author: @donaldpipowitch (Donald Pipowitch)
  • Why it matters: Closest match to a fluent, declarative URL filter config:
    • Builder API: configure().add(number('pageSize').default(10)).add(boolean('active').filter({ label: 'Active' }))
    • Strong typing via recursive generics
    • Returns filterValues, filterLabels, hasActiveFilters, resetFilters, setFilters
    • Custom .serialize() / .deserialize() per param
    • Smart side effects (filter change auto-resets pagination)
  • Gap: Solves URL↔state cleanly but has no GraphQL mapping. Would need .field() / .resolve() extensions for the second hop.

4. Refine useTable — syncWithLocation + Missing .resolve() Feature Request

  • Docs: https://refine.dev/docs/api-reference/core/hooks/useTable/
  • Feature request: refinedev/refine#6620
  • Why it matters: Refine has the closest pipeline to a full bidirectional system:
    • CrudFilter[] model: { field, operator, value } objects
    • syncWithLocation: true encodes filters into URL params
    • Data providers (e.g., @refinedev/hasura) convert CrudFilter[] → GraphQL where clause
  • Key insight: The community has explicitly requested the missing read/resolve direction (converting filters back to form values) as Issue #6620. This validates that the .resolve() pattern fills a real, recognized gap in the ecosystem.

Tier 2: Useful Building Blocks & Background

5. nuqs — Type-Safe URL State Primitives

  • Docs: https://nuqs.dev
  • GitHub: https://github.com/47ng/nuqs (~10K stars, actively maintained)
  • Role: The URL state layer. Key capabilities for filter systems:
    • useQueryStates() for atomic multi-key updates
    • Built-in parsers: parseAsArrayOf, parseAsStringLiteral, parseAsIsoDate, parseAsJson
    • Custom parsers via createParser({ parse, serialize, eq? })
    • urlKeys mapping to decouple code names from URL param names
    • Programmatic setFilters() from external data (server responses, AI suggestions)
    • createSearchParamsCache() for server-side access with shared parser definitions

6. react-admin StackedFilters

  • Docs: https://marmelab.com/react-admin/StackedFilters.html
  • Relevance: Declarative filter config with helper functions (textFilter(), numberFilter(), dateFilter()). URL persistence via JSON-encoded filter param. Convention-based field_operator naming for the write direction. No per-filter read functions.

7. Dev.to — "React Hook to Manage URL Search Params: ?mess=less"

8. Fluent Interfaces in TypeScript

9. Aurora Scharff — Advanced Search Param Filtering in Next.js

10. LogRocket — Advanced React State Management Using URL Parameters


Tier 3: Investigated but Low Relevance

Reference Reason
react-zod-url-state (GitHub) Exists but 8 stars, dormant since Jan 2025. Good DX reference, not production-worthy.
armand1m/react-query-filter (GitHub) WIP, ~20 stars. In-memory filter UI state only — no URL sync, no bidirectional mapping.
Sunny Sun — "Apply Builder Pattern To Generate Query Filter In TypeScript" (Medium) Classic GoF Builder for OData-style string concatenation. Unidirectional, no URL, no React integration.
TanStack Table + tanstack-table-search-params (GitHub) Bidirectional URL sync for table state, but no declarative filter config or query mapping layer.
react-querybuilder (Docs) Visual query builder with SQL/MongoDB export. Strong on structured query output, no URL sync.
React-SearchKit (Invenio) (GitHub) Centralized query state with URL handler. Designed for Invenio/OpenSearch, not general-purpose.

Ecosystem Comparison Matrix

Library/Pattern Fluent Builder Config Bidirectional URL Sync Maps to Query Object Per-Filter Read/Write React Context
Your architecture Yes Yes (nuqs) Yes (GraphQL) Yes (.field() / .resolve()) Yes
Harness Filters No (imperative) Yes Partial No Yes
openstatusHQ/data-table-filters Yes (schema) Yes (nuqs) No No No
Hook Builder gist Yes (fluent) Yes No No No
Refine useTable No (array) Yes Yes (data provider) No (requested in #6620) Partial
react-admin StackedFilters Yes (helpers) Yes (JSON) Via data provider No (convention) No
nuqs No (primitives) Yes No No No
TanStack Router No (infra) Yes No No No

Conclusion

The filter architecture described here is a novel composition of three well-understood patterns:

  1. Fluent builder DSL for filter configuration (prior art: Hook Builder gist, react-admin helpers)
  2. Bidirectional URL ↔ structured query mapping with explicit write/read functions per filter (prior art: Refine's pipeline, but .resolve() is their missing piece — Issue #6620)
  3. React Context bridge with toGraphQLFilter() / fromGraphQLFilter() (prior art: Harness engineering article)

Each layer exists independently in the ecosystem. The contribution is combining them into a cohesive system where filters are declared once and automatically handle URL serialization, UI rendering, and GraphQL query generation — in both directions.


Research conducted March 2026. References verified via direct URL access and source code inspection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment