
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
react-nav-query-params
Advanced tools
A react library for easily managing typed url query parameters.
A react package for easily managing query parameters across various routes throughout an application with typescript support, serialization/deserialization, and error handling. Note: This package is still in development, so there may be some bugs and major changes over time.
npm install --save react-nav-query-params
Note: This package is meant for mainly TypeScript users as it works best in TypeScript-based applications to type URL query parameters and group them based on routes. JavaScript users can still benefit from using this package, but they will not get all the advantages that the package has to offer.
// Import the function from the package
import { createNavManager } from "react-nav-query-params";
// (Suggested) Create a variable to store your route key strings
export const Routes = {
Route1: "route1",
Route2: "route2",
Route3: "route3",
};
// (Optional) Define the types of query params that are used in the application grouped by route keys used above
// keys are strings corresponding to different pages or components (route key)
// values correspond to the name and type of the query params associated with the route key (ignore if not using TypeScript (JavaScript users))
type SampleStringUnion = "one" | "two" | "three";
enum SampleEnum = {
one = "one",
two = "two",
three = "three"
}
export type QueryParamTypeMapping { // <-- * use type
[Routes.Route1]: {
param1: {[sample in SampleStringUnion]?: boolean};
param2: SampleStringUnion;
param3: number[];
param4: string;
},
[Routes.Route2]: {
param5: boolean;
param6: number;
},
[Routes.Route3]: {
param7: "first" | "second" | "third";
param8: string;
param9: SampleEnum;
}
}
// * there are limited types you can use with this package that are grouped into one
// * of two categories based on how encoding/decoding is handled
// * simple types (SimpleType) => string | number | boolean | bigint
// * complex types => Record<string, SimpleType> | Date | Array<SimpleType>
// * encoding/decoding is done based on type key (a key associated with a specific type)
// The supported type keys are listed below under the section 'List of Type Keys & Types'
const { creator, activator } = createNavManager({
customTypeKeyMapping: {}
});
// use the activator function returned to help enforce typescript's type checking/auto-completion
// activator function helps to determine the corresponding type key used for encoding/decoding given the type of each params keys
// (could also exclude the generic argument, as seen in the JS example below)
const routeMapping = activator<QueryParamTypeMapping>({
[Routes.Route1]: {
typeKeyMapping: {
param1: "booleanRecord", // <-- param key : type key (mapping)
param2: "string",
param3: "numberArray",
param4: "string",
},
programmaticNavigate: false, // for reading query params (optional)
},
[Routes.Route2]: {
typeKeyMapping: {
param5: "boolean",
param6: "number",
},
},
[Routes.Route3]: {
typeKeyMapping: {
param7: "string",
param8: "string",
param9: "stringEnum",
},
options: { // here you can specify extra options for the
// param key based on the type key that is used
param9: { // param9 is a 'stringEnum' which has an option of enumType
// to specify the valid values when encoding/decoding
enumType: Object.values(SampleEnum),
},
}
},
});
// call creator function with the routeMapping
// and you will get back a context to wrap around the application
// as well as a hook to manage the query params given a specific route key
// (Refer to the 'Usage' section below)
export const { NavQueryContext, useNavQueryParams } = creator(
routeMapping
);
// Import the function from the package
import { createNavManager } from "react-nav-query-params";
// (Suggested) Create a variable to store your route strings
export const Routes = {
Route1: "route1",
Route2: "route2",
Route3: "route3",
};
// * there are limited types you can use with this package that are grouped into one
// * of two categories based on how encoding/decoding is handled
// * simple types (SimpleType) => string | number | boolean | bigint
// * complex types => Record<string, SimpleType> | Date | Array<SimpleType>
// * encoding/decoding is done based on type key (a key associated with a specific type)
// The supported type keys are listed below under the section 'List of Type Keys & Types'
const { creator, activator } = createNavManager({
customTypeKeyMapping: {}
});
const routeMapping = activator({
[Routes.Route1]: {
typeKeyMapping: {
param1: "booleanRecord", // <-- param key : type key (mapping)
param2: "string",
param3: "numberArray",
param4: "string",
},
programmaticNavigate: false, // for reading query params (optional)
},
[Routes.Route2]: {
typeKeyMapping: {
param5: "boolean",
param6: "number",
},
},
[Routes.Route3]: {
typeKeyMapping: {
param7: "string",
param8: "string",
param9: "stringEnum",
},
options: { // here you can specify extra options for the
// param key based on the type key that is used
param9: { // param9 is a 'stringEnum' which has an option of enumType
// to specify the valid values when encoding/decoding
enumType: ["one", "two", "three"],
},
}
},
});
// call creator function with the routeMapping
// and you will get back a context to wrap around the application
// as well as a hook to manage the query params given a specific route key
// (Refer to the 'Usage' section below)
export const { NavQueryContext, useNavQueryParams } = creator(
routeMapping
);
// (example below uses react-router v6, and will look slightly different for other routing solutions)
// (This example is much more complicated because of how react-router v6 defines routes
// and how it prevents access to the location and history object from the components that are not used inside the main Browser router object)
// (For other routing solutions, such as Next.js router the setup may be easier)
// The main purpose of this step is to provide the package access to the location and history objects
// used by your routing solution so that the package can read and update the query params
// If you are using react-router v6, add a component 'RouteBasePage' to the root of your router with the path '/'
// that uses the 'NavQueryContext' given by this package (look below for the implementation of RouteBasePage)
const router = createBrowserRouter([ // from react-router v6
{
path: "/",
element: <RouteBasePage />,
children: [{
path: "/",
element: <ReadingParams />,
index: true,
},
{
path: "/route1",
element: <NavigatingAndSettingParams />
}]
}
])
// In the root App component...
function App(){
return (
<div className="App">
<RouterProvider router={router} /> {/* from react-router v6 */}
</div>
);
}
// In a child component of the app component need to create an adapter object that tells this package how to manage history and location objects.
// need access to the history.push/replace and location data from the react-router v6
import { Adapter } from "react-nav-query-params"; //<-- TS users only
import { useLocation, useNavigate, Outlet } from "react-router-dom"; //<-- react router v6 users only
function RouteBasePage(){
// The following is specific to react-router v6 and might look different for each routing solution,
// but the adapter created needs to be provided in a similar manner in the NavQueryContext.Provider
// or else the useNavQueryParams hook will not work
const location = useLocation(); // <-- for react-router, can only be called in a child component of RouterProvider
const navigate = useNavigate(); // <-- for react-router, can only be called in a child component of RouterProvider
const adapter = useMemo(() => { //<-- JS users, const adapter = useMemo<Adapter>(() => { //<-- TS users
return {
location: location,
pushLocation: (l) => { // the type of 'l' here is Adapter["location"] for TS users
if (l.search !== null || l.search !== undefined)
navigate("?" + l.search, { replace: false });
},
replaceLocation: (l) => {
if (l.search !== null || l.search !== undefined)
navigate("?" + l.search, { replace: true });
},
};
}, [location, navigate]);
return (
<NavQueryContext.Provider
value={{
adapter: adapter,
}}
>
<Outlet />
</NavQueryContext.Provider>
);
}
// A component where you read the query params
function ReadingParams(){
const { getQueryParams: getQueryParamsRoute1, clearQueryParams: clearQueryParamsRoute1 } = useNavQueryParams(Routes.Route1);
// All functions returned by the hook are memoized using useCallback,
// getQueryParams has a dependency on the query string used in the URL
// so if the query string changes, all dependency arrays with the
// function as a dependency will rerun
// url: localhost:3000/route1?param1=%7B%22one%22%3Atrue%7D¶m2=three
const queryParams = useMemo(() => {
return getQueryParamsRoute1();
},[getQueryParamsRoute1])
console.log(
"Value of param2 query param is 'two': ",
queryParams.values?.param2 === "two") //<-- false
// getQueryParamsRoute1 function will get the query params that
// are associated with the 'route1' route key which is mapped to the
// the url path route1
return (
<div>
{/* The below button is optional */}
<button
// clearQueryParamsRoute1 clears all query params from
// url associated with route1
onClick={() => { clearQueryParamsRoute1() }}
>
Clear Params
</button>
Check the console to see the query params
</div>
)
}
// In some other component...
function NavigatingAndSettingParams() {
const { getQueryString } = useNavQueryParams(Routes.Route1);
const queryString = getQueryString({ param2: "three", param1: { "three": true }, param3: [10, 30] },
{ // (optional argument)
replaceAllParams: true, // replace the current query params, when getting the query string
full: true, // include the '?' in the query string
});
const navigate = useNavigate(); // from react router v6
// url: localhost:3000/route1?param2=three¶m1=%7B%three%22%3Atrue%7D&
// param3=10%2C30
return (
<div>
<button onClick={() => navigate("/route1" + queryString)}>Click Me</button>
</div>
);
}
// (It is possible to create your own type keys / types as well as options)
// (Look below at the Custom Types / Type Keys section)
type TypeKeys = {
// simple types
"string": string;
"number": number;
"boolean": boolean;
// complex types
// array types
"stringArray": string[];
"numberArray": number[];
"booleanArray": boolean[];
// record types
"stringRecord": Record<string, string>;
"numberRecord": Record<string, number>;
"booleanRecord": Record<string, boolean>;
// enums
"stringEnum": string;
"numberEnum": number;
// date
"date": Date;
};
type TypeKeyToOptions = {
// array types
stringArray: {
separator: string; // specify the separator used when encoding/decoding
// the array i.e. '*' rather than ','(%2C)
expanded?: boolean; // set to true to encode in the long form (i.e. param3=20¶m3=40)
// rather than short form (param3=20,40)
};
numberArray: {
separator: string;
expanded?: boolean;
};
booleanArray: {
separator: string;
expanded?: boolean;
};
// record types
stringRecord: {
objectStartSeparator?: string; // replaces the objectStartSeparator character'{' when
// encoding/decoding
objectEndSeparator?: string;
objectEntrySeparator?: string;
keyValueSeparator?: string;
};
numberRecord: {
objectStartSeparator?: string;
objectEndSeparator?: string;
objectEntrySeparator?: string;
keyValueSeparator?: string;
};
booleanRecord: {
objectStartSeparator?: string;
objectEndSeparator?: string;
objectEntrySeparator?: string;
keyValueSeparator?: string;
};
stringEnum: {
enumType: string[]; // specify the allowed values used when encoding/decoding the enum
// i.e. Object.values(EnumType) where all the values have
// to extends the string type
};
numberEnum: {
enumType: number[];
};
// date
date: {
format?: "ISO", // specify the format (currently the only supported format is ISO)
hyphenSeperator: string; // replaces the hyphenSeperator character'-' when
// encoding/decoding the ISO date
colonSeperator: string;
};
};
function isNumericRange(value: unknown): value is [number, number] {
return Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number";
}
const { creator, activator } = createNavManager<{
numericRange: [number, number]; // <-- name: type of custom type key
},{
numericRange: { expanded?: boolean, test: string }, // <-- name: options (must extend object type) of custom typekey
}>
({
customTypeKeyMapping: { // <- define custom type keys to encode and decode
numericRange: {
category: "custom", // <- always use "custom"
defaultValue: [0, 1] // <-- optional default value(used for type setting)
encodingMap: { // <- specify a way to encode/decode type associated with the custom type key
encode: (value, options) => { // <-- encode funtion takes in a value matching the type ([number, number]) and must return a string or string[]
return JSON.stringify(value);
},
decode: (value, options) => { // <-- decode funtion takes in a string or string[] and
// must return a value matching the type ([number, number])
// <- You can also throw an Error inside the decode function saying the string
// value cannot be decoded
let valueToDecode = "";
if (Array.IsArray(value)){
// value here is string[],(i.e. param3=20%2C3¶m3=40%2C5 =>
// ['[20,3]','[40,5]']) so get the first entry (or use all the entries if you prefer
// by checking the expanded property from the options object)
valueToDecode = value[0];
}
const decoded = JSON.parse(valueToDecode);
if (!isNumericRange(decoded)) throw new Error("Error while decoding");
return decoded; // <-- [20, 3]
},
encodingOptions: { // <- optionally specify default options for encoding/decoding
expanded: true,
test: "test",
},
},
match: (value, options) => { // <- optianally specify a way to match for the type key given an unknown value (for error handling)
// the options here are the defaults specified in the encodingOptions above not the options passed to the param key
return isNumericRange(value);
},
},
}
});
creator() / activator() //<-- now have access to custom type/type key
// Creating custom type keys with this package using only JavaScript is very difficult because
// javascript does not allow you to specify types (niether does it enforce type checking)
// and any custom type key you create with have the associated type of any, but you can still use
// the package to create type keys that enforce/validate the query params
function isNumericRange(value) {
return Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number";
}
const { creator, activator } = createNavManager({
customTypeKeyMapping: { // <- define custom type keys to encode and decode
numericRange: {
category: "custom", // <- always use "custom"
defaultValue: [0, 1], // <-- optional default value(used for type setting)
encodingMap: { // <- specify a way to encode/decode type associated with the custom type key
encode: (value, options) => { // <-- encode funtion takes in a value matching the type (any) and must return a string or string[]
return JSON.stringify(value);
},
decode: (value, options) => { // <-- decode funtion takes in a string or string[] and
// must return a value matching the type (any)
// <- You can also throw an Error inside the decode function saying the string
// value cannot be decoded
let valueToDecode = "";
if (Array.IsArray(value)){
// value here is string[],(i.e. param3=20%2C3¶m3=40%2C5 =>
// ['[20,3]','[40,5]']) so get the first entry (or use all the entries if you prefer
// by checking the expanded property from the options object)
valueToDecode = value[0];
}
const decoded = JSON.parse(valueToDecode);
if (!isNumericRange(decoded)) throw new Error("Error while decoding");
return decoded; // <-- [20, 3]
},
encodingOptions: { // <- optionally specify default options for encoding/decoding
expanded: true,
test: "test",
},
},
match: (value, options) => { // <- optianally specify a way to match for the type key given an unknown value (for error handling)
// the options here are the defaults specified in the encodingOptions above not the options passed to the param key
return isNumericRange(value);
},
},
}
});
creator() / activator() //<-- now have access to custom type/type key
FAQs
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.