@demershov/use-route-query
v0.0.5
Published
A Vue 3 composable for synchronizing route query parameters with reactive state.
Maintainers
Readme
useRouteQuery
A small Vue 3 composable for syncing a reactive value with the route query while avoiding unnecessary object churn from transformer functions.
This library was inspired by @vueuse/router's useRouteQuery. The motivating issue is that when a transformer (the transform option) returns a non-primitive (an object or array), it may produce a new object instance even if its contents haven't changed. Since Vue compares refs by identity, returning a new object causes downstream components that receive the reactive value as a prop to re-render even when the logical value is unchanged.
The author found this behavior undesirable and implemented this library to avoid always returning a new object when a transformer produces an object that is semantically equal to the previous one.
Additional motivation and typing differences:
- In
@vueuse/routerthe default value is often treated as a primitive. In contrast, this implementation typesdefaultValueasT(matching the value returned bytransform.get). That allows passing typed defaults (objects or primitives) so the returnedRefhas a clearTtype. - In this implementation the URL may contain the default value if the user explicitly sets it. That means default values are not implicitly hidden from the URL — if your app writes the default into the query, it will appear in the route.
For more details and discussion about the underlying problem see:
- https://github.com/vuejs/docs/issues/2884
- https://github.com/vueuse/vueuse/issues/3992
Key idea
- Provide a composable
useRouteQuery(name, defaultValue?, options?)that returns aRef<T>. - If you supply
options.transform.get/options.transform.set, the library uses them but keeps an internal cache of the raw query and compares raw values to avoid exposing a new object when the logical value hasn't changed.
API
function useRouteQuery<T>(
name: string,
defaultValue?: MaybeRefOrGetter<T>,
options?: RouteQueryOptions<T>
): Ref<T>name— query parameter namedefaultValue— default value when the query param is absentoptions— optional settings:mode—'push' | 'replace'(defaults to'push')route— custom route (defaults touseRoute())router— custom router (defaults touseRouter())transform—RouteQueryTransform<T>:{ get?: (value: GetQueryValue) => T, set?: (value: T) => SetQueryValue }
Examples
Basic usage (string query):
import { useRouteQuery } from "useRouteQuery";
const q = useRouteQuery("q", "");
// q is a Ref<string>
// read
console.log(q.value);
// write
q.value = "search term";Using a transformer for a primitive:
import { useRouteQuery } from "useRouteQuery";
const page = useRouteQuery<number>("page", 1, {
transform: {
get: (v) => Number(v),
set: (n) => String(n),
},
});
// page.value is a numberUsing a transformer that returns an object (safe from unnecessary rerenders):
import { useRouteQuery } from "useRouteQuery";
const filter = useRouteQuery("filter", { text: "" }, {
transform: {
get: (v) => {
// parse query string into an object
if (!v) return { text: "" };
try {
return JSON.parse(Array.isArray(v) ? v[0] : v);
} catch {
return { text: String(v) };
}
},
set: (obj) => {
try {
return JSON.stringify(obj);
} catch {
return undefined;
}
},
},
});
// Because this library caches the raw query and checks equality,
// components won't re-render unnecessarily when the transformer returns
// a new but equivalent object instance.Typed-default examples
// typed default object — returned ref has type { text: string }
const filter = useRouteQuery("filter", { text: "" }, {
transform: {
get: (v) => {
if (!v) return { text: "" } as const;
try {
return JSON.parse(Array.isArray(v) ? v[0] : v);
} catch {
return { text: String(v) };
}
},
set: (obj) => JSON.stringify(obj),
},
});
// primitive default example — returned ref has type number
const page = useRouteQuery<number>("page", 1, {
transform: {
get: (v) => Number(v),
set: (n) => String(n),
},
});Default-in-URL behavior (short demo)
import { useRouteQuery } from "useRouteQuery";
// Suppose we use a JSON transformer as above
const filter = useRouteQuery("filter", { text: "" }, {
transform: {
get: (v) => {
if (!v) return { text: "" };
try {
return JSON.parse(Array.isArray(v) ? v[0] : v);
} catch {
return { text: String(v) };
}
},
set: (obj) => JSON.stringify(obj),
},
});
// If the user writes the default value explicitly, this implementation
// will write it into the URL (e.g. ?filter=%7B%22text%22%3A%22%22%7D).
filter.value = { text: "" };
// By contrast, vueuse's `useRouteQuery` will convert a value equal to
// the default into `undefined` before writing, causing the query param
// to be omitted from the URL.Notes
- This composable intentionally avoids exposing low-level internals. It returns a
Ref<T>that you can read/write as usual. - The library compares raw query values (after
transform.set) with a cached raw value to avoid enqueuing router updates or triggering ref updates when the effective value is unchanged.
License
MIT
