@promoboxx/use-filter
v1.11.2
Published
A React hook to easily build filter views, with features like:
Downloads
82
Maintainers
Keywords
Readme
@promoboxx/use-filter
A React hook to easily build filter views, with features like:
- persistence
- debouncing
- cursor based pagination
- offset / page based pagination
What's Included
The filter system providers you with a few things:
filter
: A key/value store for arbitrary filter valuesfilterInfo
: Metadata around your filter with the current page, number of results, total pages, cursor, etcfilterApi
: What is returned from the hook, complete withfilterInfo
and helpers to update values in yourfilter
, change the page, etc.
Usage
There are two hooks provided. The default, "advanced", is more controlled, where you provide an onChange
function which is called by the hook whenever you manipulate the filter.
The alternative, "simple", works similarly but gives you both filterInfo
and a debouncedFilterInfo
for you to use in effects.
Advanced Example
const { filterInfo, updateFilter, setPage, resetFilter, isLoading } = useFilter(
// Since this deals with caching, a namespace is required.
'advanced',
{
// Disable this if you are using the hook's data caching.
shouldForceRunOnMount: true,
defaultFilterInfo: {
filter: {
name: '',
},
pageSize: 20,
},
// Called by the hook whenever the filter changes.
async onChange(
filterInfo,
// If this is called as a result of the filter changing, the reason will
// be 'filter', If it's due to the page changing, it will be 'pagination'.
updateReason,
) {
const { data } = await runQuery({
name: filterInfo.filter.name,
page: filterInfo.page,
})
// Here we are telling the filter system how many results there are. Since
// it knows how many we want to display per-page, it can now figure out
// how many pages there are in total.
return {
filterInfo: {
totalResults: data?.locations?.info.count,
},
}
},
},
)
return (
<>
<input
type="text"
value={filterInfo.filter.name}
onChange={(event) => {
updateFilter({ name: event.target.value })
}}
/>
<button onClick={resetFilter}>Reset</button>
{isLoading ? <CircularProgress /> : undefined}
{/* Hypothetical list of results would be here */}
<Pagination
totalPages={filterInfo.totalPages}
page={filterInfo.page}
onPageChange={(page) => {
setPage(page)
}}
/>
</>
)
Simple Example
const {
filterInfo,
debouncedFilterInfo,
updateFilter,
pagingInfo,
setPage,
resetFilter,
isLoading,
} = useSimpleFilter('simple', {
defaultFilterInfo: {
filter: {
name: '',
},
pageSize: 20,
},
})
// Hypothetical fetch / query / graphql hook.
const { data, fetch } = useQuery()
useEffect(() => {
fetch({
name: debouncedFilterInfo.filter.name,
page: debouncedFilterInfo.page,
})
}, [debouncedFilterInfo])
// Since the simple mode doesn't know when we're done doing our work, it
// provides a helper to get us pagination info.
const { totalPages } = pagingInfo(data?.count)
return (
<>
<input
type="text"
value={filterInfo.filter.name}
onChange={(event) => {
updateFilter({ name: event.target.value })
}}
/>
<button onClick={resetFilter}>Reset</button>
{isLoading ? <CircularProgress /> : undefined}
{/* Hypothetical list of results would be here */}
<Pagination
totalPages={totalPages}
page={filterInfo.page}
onPageChange={(page) => {
setPage(page)
}}
/>
</>
)
API
useFilter(options)
interface UseFilterOptions<TFilter, TResult> {
/**
* Default values for your filter. When calling `.reset` your filter will be
* set to this.
* Changing these values does not cause a call to happen.
*/
defaultFilterInfo?: Partial<FilterInfo<TFilter>>
/**
* Enable this if you are not using the data caching of this hook.
*/
shouldForceRunOnMount?: boolean
/**
* Called whenever the filter changes.
*/
onChange: (
filterInfo: FilterInfo<TFilter>,
reason: UseFilterUpdateReason,
) => MaybePromise<UseFilterOnChangeResult<TFilter, TResult>>
/**
* In case you want to change the default debounce duration.
*/
debounceDuration?: number
}
export interface FilterApi<TFilter, TResult> {
/**
* Whether the system is debouncing or waiting for your `onChange` to finish.
*/
isLoading: boolean
/**
* Any cached data for this filter.
*/
data: TResult | null | undefined
/**
* Access to the filter and its metadata.
*/
filterInfo: FilterInfo<TFilter>
/**
* Whether the filter existed in the system when the component mounted.
* @deprecated
*/
doesFilterExist: boolean
/**
* Why the last update happened.
*/
updateReason: UseFilterUpdateReason
/**
* Update one or more values in your filter.
*/
updateFilter: (
filter: Partial<TFilter>,
shouldRunImmediately?: boolean,
) => void
/**
* Resets the filter back to `defaultFilterInfo`.
*/
resetFilter: (shouldRunImmediately?: boolean) => void
/**
* Changes the offset. Will update `page`.
*/
setOffset: (offset: number | string, shouldRunImmediately?: boolean) => void
/**
* Changes the page. Will update `offset`.
*/
setPage: (page: number | string, shouldRunImmediately?: boolean) => void
/**
* Changes the page size.
*/
setPageSize: (
pageSize: number | string,
shouldRunImmediately?: boolean,
) => void
/**
* Change the sort method.
*/
setSort: (sort: string | undefined, shouldRunImmediately?: boolean) => void
/**
* Changes the cursor.
*/
setCursor: (cursor: string | null | undefined) => void
/**
* Forces a refresh of the filter.
*/
forceRefresh: (shouldRunImmediately?: boolean) => void
}
interface FilterInfo<TFilter> {
filter: TFilter
offset: number
page: number
sort?: string
pageSize: number
lastRefreshAt: number
totalResults: number
totalPages: number
shouldRunImmediately: boolean
cursor?: string | null
nextCursor?: string | null
}
useSimpleFilter(options)
interface UseSimpleFilterOptions<TFilter> {
/**
* Default values for your filter. When calling `.reset` your filter will be
* set to this.
* Changing these values does not cause a call to happen.
*/
defaultFilterInfo?: Partial<SimpleFilterInfo<TFilter>>
/**
* In case you want to change the default debounce duration.
*/
debounceDuration?: number
}
export interface SimpleFilterApi<TFilter> {
/**
* Really more "is debouncing", since simple mode doesn't know when your code
* is doing anything.
*/
isLoading: boolean
/**
* Access to the filter and its metadata.
*/
filterInfo: SimpleFilterInfo<TFilter>
/**
* Same as the regular `filterInfo`, but updates in a debounced fashion.
*/
debouncedFilterInfo: SimpleFilterInfo<TFilter>
/**
* Why the last update happened.
*/
updateReason: UseFilterUpdateReason
/**
* Update one or more values in your filter.
*/
updateFilter: (
filter: Partial<TFilter>,
shouldRunImmediately?: boolean,
) => void
/**
* Does the boring math for you based on your `pageSize` and `totalResults`.
*/
pagingInfo: (total: string | number | null | undefined) => {
totalResults: number
totalPages: number
}
/**
* Resets the filter back to `defaultFilterInfo`.
*/
resetFilter: (shouldRunImmediately?: boolean) => void
/**
* Changes the offset. Will update `page`.
*/
setOffset: (offset: number | string, shouldRunImmediately?: boolean) => void
/**
* Changes the page. Will update `offset`.
*/
setPage: (page: number | string, shouldRunImmediately?: boolean) => void
/**
* Changes the page size.
*/
setPageSize: (
pageSize: number | string,
shouldRunImmediately?: boolean,
) => void
/**
* Change the sort method.
*/
setSort: (sort: string, shouldRunImmediately?: boolean) => void
/**
* Changes the cursor.
*/
setCursor: (
cursor: string | null | undefined,
shouldRunImmediately?: boolean,
) => void
/**
* Forces a refresh of the filter.
*/
forceRefresh: (shouldRunImmediately?: boolean) => void
}
interface SimpleFilterInfo<TFilter> {
filter: TFilter
sort?: string
offset: number
page: number
pageSize: number
lastRefreshAt: number
cursor?: string | null
shouldRunImmediately: boolean
}
Stores
Persistence is provided by "stores". use-filter comes with a few included:
memoryStore
: For when you want persistence across page views in your app, but not refreshes.localStorageStore
: For when you want full persistence of filters.reduxStore
: For when you want full persistence of filters and any extra goodies your redux middleware might bring you.
To use a store, simply:
import { setFilterStore } from '@promoboxx/use-filter/dist/store'
import localStorageStore from '@promoboxx/use-filter/dist/store/localStorageStore'
setFilterStore(localStorageStore)
Creating a store
If you would like to build your own, a store must match this interface:
interface FilterStore {
getFilter<TFilter = any>(
namespace: string,
): FilterInfo<TFilter> | null | undefined
saveFilter<TFilter = any>(
namespace: string,
filter: FilterInfo<TFilter>,
): void
getData<TResult = any>(namespace: string): TResult | null | undefined
saveData<TResult = any>(namespace: string, data: TResult): void
clear(): void
}