npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@youha-eng/next-query-utils

v0.1.1

Published

Youha specific utilities for using next-query-state to persist page's current state in the URL.

Downloads

4

Readme

@youha-eng/next-query-utils

NPM Bundle size GitHub license

Youha specific utilities for using next-query-state to persist page's current state in the URL.

Contains code specific for Youha's API (useFilter).

Installation

$ yarn add @youha-eng/next-query-utils next-query-state
or
$ npm install @youha-eng/next-query-utils next-query-state

Documentation

usePagination

Utility hook for pagination state control.

  • Save state in the URL as "page" and "pageSize" query parameters.
  • page and pageSize converted into limit and offset for easier API calls.
  • page and pageSize can be updated via setPagination.
  • Using changePageSize to update pageSize automatically changes page so that the first item remains visible on the screen.
const [{ page, pageSize, limit, offset }, setPagination, changePageSize] = usePagination({
    defaultPageSize: 20,
});

const onPageChange = (e) => {
    setPagination({ page: parseInt(e.currentTarget.value) }, { history: "push" });
};

const onPageSizeChange = (e) => {
    changePageSize(parseInt(e.currentTarget.value));
};

return (
    <div>
        <div>
            page: <input value={page} onChange={onPageChange} />
        </div>
        <div>
            page size:
            <select value={pageSize} onChange={onPageSizeChange}>
                <option value="20">20</option>
                <option value="50">50</option>
                <option value="100">100</option>
            </select>
        </div>
    </div>
);

useSort

Utility hook for sort state control.

  • Returned value always starts with '+' or '-'.
  • Allowed values can be set with allowed parameter. '+' or '-' prefix only allows that sort direction, and no prefix allows both '+' and '-'.
  • showPlus option is for showing '+' in the URL or not. No prefix implies ascending order. Default is false because '+' gets percent encoded and makes URL look dirty in current implementation.
  • delimiter option sets which delimiter to use for separating sort strings. Set to null for using duplicate keys instead of delimited string. Defaults to "_".
  • By default, defaultSort, allowed, showPlus, delimiter option must not be changed. Set dynamic option to true to change those.
const [sort, setSort] = useSort({
    defaultSort: ["+fieldA"],
    allowed: ["fieldB", "+fieldA"],
    history: "push",
    showPlus: true,
    delimiter: "_",
});

return (
    <div>
        {["+a", "-a", "+b", "-b"].map((field) => (
            <button key={field} onClick={() => setSort((p) => [...p, field as SortType])}>
                {field}
            </button>
        ))}
        {sort.map((v, i) => (
            <div key={i}>{v}</div>
        ))}
    </div>
);

useFilter

Utility hook for constructing filter expression by reading from URL state.

  • Filter expression is a list of 3-tuple made up of field name, filter operator, and filter value.
    • For example, ?rangeMin=12345&rangeMax=6431&search=foo&category=a&category=b becomes:
      [["search","=","foo"],
       ["range","<=",6431],
       ["range",">=",12345],
       ["category","=",["a","b"]]]
  • useFilter takes a list of FilterDef, which is made up of Map of Serializers<T>, and a transform function that takes the parsed state object and creates a list of filter expression which will be concatenated.
  • useFilter returns the concatenated filter expression list.
  • FilterDef can be manually created, but a preset factory utility filterDefFactory and filterTypes is provided for convenience.

Preset

Since useFilter takes a list of FilterDef instead of a map, a function to make map into list is needed, which is filterDefFactory.

When using preset and custom created FilterDef together, spread the return value of filterDefFactory like below.

useFilter([
    ...filterDefFactory({
        search: filterTypes.nullable.string.equal(),
        range: filterTypes.integer.range(),
        category: filterTypes.enum(["a", "b", "c"]).in(),
    }),
    // Custom FilterDef
    {
        queryType: ...,
        transformer: ()=>{}
    }
])

filterDefFactory takes a map of FilterGenerator. its Key is the name of the field, and value is FilterGenerator. FilterGenerator can be easily constructed from filterTypes.

To use filterTypes, select the data type of the field, then select which kind of filter (equal, range, in) it is.

There are 5 types available, string, float, integer, boolean, enum. You can also have null as a value like this: filterTypes.nullable.string, which the null value is represented in the URL as %00.

Then, select which kind of filter is enabled.

"equal" only allows 1 value, which will result to filter expression like this: [[field, "=", value]]

"range" will read from 2 query params with postfix "Min" and "Max" (for example ?fieldMin=10&fieldMax=20), and will result to filter expression like this: [[field, ">=". fieldMin], [field, "<=", fieldMax]]. It includes the filter expression only if it's in the query string.

  • There is a boolean excludeNull option that adds [field, "!=", null] filter expression when min or max filter exists.

"in" allows many values, and will result to filter expression like this: [[field, "=", values]]

  • delimiter option can be used to set which delimiter should be used to separate multiple values in the URL. Default is undefined which doesn't use delimiter and uses duplicate query keys to express list of values. (?field=a&field=b)

Example: Read filter state in URL with useFilter to show filtered results

function FilterResults() {
    const filters = useFilter(
        filterDefFactory({
            search: filterTypes.nullable.string.equal(),
            range: filterTypes.integer.range(),
            category: filterTypes.enum(["a", "b", "c"]).in(),
        })
    );

    // Send filters as API request to server and show its response.
    const data = getDataFromServer(filters)

    return (
        <div>
            {data.map((d)=><DataPresenter data={d}>)}
        </div>
    );
}

Example: Control filter state with useQueryState

function FilterPanel() {
    const [search, setSearch] = useQueryState("search", queryTypes.string);
    const [rangeMin, setRangeMin] = useQueryState("rangeMin", queryTypes.integer);
    const [rangeMax, setrangeMax] = useQueryState("rangeMax", queryTypes.integer);
    const [categories, setCategories] = useQueryState(
        "category",
        queryTypes.array(queryTypes.stringEnum(["a", "b", "c"])).withDefault([])
    );

    return (
        <div>
            <div>
                search:
                <input
                    value={search ?? ""}
                    onChange={(e) => setSearch(e.currentTarget.value || null)}
                />
            </div>
            <div>
                range:
                <input
                    value={rangeMin ?? ""}
                    onChange={(e) => setRangeMin(parseIntOrNull(e.currentTarget.value))}
                />
                <input
                    value={rangeMax ?? ""}
                    onChange={(e) => setrangeMax(parseIntOrNull(e.currentTarget.value))}
                />
            </div>
            <div>
                categories:
                {["a", "b", "c"].map((v) => (
                    <span key={v}>
                        <input
                            type="checkbox"
                            id={v}
                            checked={categories.includes(v)}
                            onChange={(e) =>
                                e.currentTarget.checked
                                    ? setCategories([...categories, v])
                                    : setCategories(categories.filter((c) => c !== v))
                            }
                        />
                        <label htmlFor={v}>{v}</label>
                    </span>
                ))}
            </div>
        </div>
    );
}

function parseIntOrNull(s: string) {
    const int = parseInt(s);
    return isNaN(int) ? null : int;
}