- {'This page has a default value of "world" for the key "hello".'}
+ {
+ 'This page has a default value of "world" for the key "hello". The key/value pair is removed from URL if its set to the default value.'
+ }
diff --git a/apps/playground/src/app/with-weak-typesafety/page.tsx b/apps/playground/src/app/with-weak-typesafety/page.tsx
index ee5b802..cd5fa5f 100644
--- a/apps/playground/src/app/with-weak-typesafety/page.tsx
+++ b/apps/playground/src/app/with-weak-typesafety/page.tsx
@@ -3,9 +3,10 @@
import { useState } from "react";
import { SetKeyValueInputs } from "@/components/set-key-value-inputs";
import { Alert } from "@/components/ui/alert";
-import { useObserveAndStore } from "@sp-hooks/next";
-interface SearchParamsType extends Record
{
+import { SPHooksStateType, useObserveAndStore } from "@sp-hooks/next";
+
+interface SearchParamsType extends SPHooksStateType {
page: string;
search: string;
testArray: string[];
diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts
index 6de0340..6f2de44 100644
--- a/packages/next/src/index.ts
+++ b/packages/next/src/index.ts
@@ -1,36 +1,23 @@
-import { usePathname, useRouter, useSearchParams } from "next/navigation";
-import {
- useObserveAndStore as useObserveAndStoreReact,
- type SupportedValues,
-} from "@sp-hooks/react";
+import { usePathname, useRouter } from "next/navigation";
-// export const useSearchParamsState = <
-// S extends Partial>,
-// >(
-// opts?: UseSearchParamsStateOptions,
-// ) => {
-// const searchParams = useSearchParams();
-// const router = useRouter();
-// const pathname = usePathname();
+import { useObserveAndStore as useObserveAndStoreReact } from "@sp-hooks/react";
+import type { SPHooksStateType } from "@sp-hooks/react";
-// const setSearchParams = (newSearchParams: URLSearchParams) => {
-// router.push(pathname + "?" + newSearchParams.toString());
-// };
+export * from "@sp-hooks/react";
+export type * from "@sp-hooks/react";
-// return useSearchParamsStateReact({
-// searchParams,
-// setSearchParams,
-// ...opts,
-// });
-// };
-
-export function useObserveAndStore>(
+export function useObserveAndStore(
state: S,
+ options?: { defaultValues?: Partial },
) {
const router = useRouter();
const pathname = usePathname();
- useObserveAndStoreReact(state, (newSearchParams) => {
- router.push(pathname + "?" + newSearchParams.toString());
- });
+ useObserveAndStoreReact(
+ state,
+ (newSearchParams) => {
+ router.push(pathname + "?" + newSearchParams.toString());
+ },
+ options,
+ );
}
diff --git a/packages/react/src/hooks.ts b/packages/react/src/hooks.ts
deleted file mode 100644
index 861ee06..0000000
--- a/packages/react/src/hooks.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-import { useState } from "react";
-
-import type { SupportedValueTypes, UseSearchParamsStateParams } from "./types";
-import { searchParamsToObject } from "./utils";
-
-export * from "./types";
-
-// --------------- Number Getter ---------------
-
-type NumberGetterResult = Readonly<{
- catch: (cval: number) => number;
- value: () => number;
-}>;
-
-const createNumberGetter = ({
- value,
-}: {
- value?: SupportedValueTypes;
-}): NumberGetterResult => {
- const v = Number(value);
-
- return Object.freeze({
- catch: (cval) => (isNaN(v) || typeof value === "undefined" ? cval : v),
- value: () => v,
- });
-};
-
-type AllNumberGetterResult = Readonly<{
- catch: (cval: number[]) => number[];
- value: () => number[];
-}>;
-
-const createAllNumberGetter = ({
- values,
-}: {
- values: SupportedValueTypes[];
-}): AllNumberGetterResult => {
- const v = values.map((v) => Number(v));
-
- return Object.freeze({
- catch: (cval) => (v.every((v) => isNaN(v)) ? cval : v),
- value: () => v,
- });
-};
-
-// ---------------- Boolean Getter --------------
-
-type BooleanGetterResult = Readonly<{
- catch: (cval: boolean) => boolean;
- value: () => boolean;
-}>;
-
-const createBooleanGetter = ({
- value,
-}: {
- value?: SupportedValueTypes;
-}): BooleanGetterResult => {
- const v = Boolean(value);
-
- return Object.freeze({
- catch: (cval) => {
- const isBoolean =
- typeof value === "boolean" || value === "true" || value === "false";
-
- return isBoolean ? v : cval;
- },
- value: () => v,
- });
-};
-
-type AllBooleanGetterResult = Readonly<{
- catch: (cval: boolean[]) => boolean[];
- value: () => boolean[];
-}>;
-
-const createAllBooleanGetter = ({
- values,
-}: {
- values: SupportedValueTypes[];
-}): AllBooleanGetterResult => {
- const v = values.map((v) => Boolean(v));
-
- const isBoolean = values.every(
- (vb) => typeof vb === "boolean" || vb === "true" || vb === "false",
- );
-
- return Object.freeze({
- catch: (cval) => (isBoolean ? v : cval),
- value: () => v,
- });
-};
-
-// ----------------- Date Getter -------------
-
-type DateGetterResult = Readonly<{
- catch: (cval: Date) => Date;
- value: () => Date | undefined;
-}>;
-
-const createDateGetter = ({
- value,
-}: {
- value?: SupportedValueTypes;
-}): DateGetterResult => {
- const vn = Date.parse(value as string);
- const v = new Date(value as string);
-
- return Object.freeze({
- catch: (cval: Date) => (isNaN(vn) ? cval : v),
- value: () => (typeof value !== "undefined" ? v : value),
- });
-};
-
-type AllDateGetterResult = Readonly<{
- catch: (cval: Date[]) => Date[];
- value: () => Date[];
-}>;
-
-const createAllDateGetter = ({
- values,
-}: {
- values: SupportedValueTypes[];
-}): AllDateGetterResult => {
- const vn = values.map((v) => Date.parse(v as string));
- const v = values.map((v) => new Date(v as string));
-
- return Object.freeze({
- catch: (cval: Date[]) => (vn.every((v) => isNaN(v)) ? cval : v),
- value: () => v,
- });
-};
-
-// ---------------- General Getter ----------------
-
-type GetterResult<
- R,
- O extends "asNumber" | "asBoolean" | "asDate" | "value" = never,
-> = Omit<
- {
- asNumber: () => NumberGetterResult;
- asBoolean: () => BooleanGetterResult;
- asDate: () => DateGetterResult;
- value: () => R | undefined;
- },
- O
->;
-
-type AllGetterResult<
- R,
- O extends "asNumber" | "asBoolean" | "asDate" | "value" = never,
-> = Readonly<
- Omit<
- {
- asNumber: () => AllNumberGetterResult;
- asBoolean: () => AllBooleanGetterResult;
- asDate: () => AllDateGetterResult;
- value: () => R[];
- },
- O
- >
->;
-
-// ----------------- createGetter -----------------
-
-const createGetter = ({
- value,
-}: {
- value?: R;
-}): GetterResult => {
- return Object.freeze({
- asNumber: () => createNumberGetter({ value }),
- asBoolean: () => createBooleanGetter({ value }),
- asDate: () => createDateGetter({ value }),
- value: () => value,
- });
-};
-
-// ----------------- createAllGetter -----------------
-
-const createAllGetter = ({
- values,
-}: {
- values: R[];
-}): AllGetterResult => {
- return Object.freeze({
- asNumber: () => createAllNumberGetter({ values }),
- asBoolean: () => createAllBooleanGetter({ values }),
- asDate: () => createAllDateGetter({ values }),
- value: () => values,
- });
-};
-
-// ----------------- createSetter -----------------
-
-type SetterResult = Readonly<{
- number: (value: number | number[]) => void;
- boolean: (value: boolean | boolean[]) => void;
- date: (value: Date | Date[]) => void;
- string: (value: string | string[]) => void;
-}>;
-
-const createSetter = ({ key }: { key: string }): SetterResult => {
- return Object.freeze({
- string: (value: string | string[]) => {
- const sp = new URLSearchParams();
-
- if (Array.isArray(value)) {
- // setStringArray;
- // sp.set(key, firstValue);
- // sp.append(key, values[i+1]);
- } else {
- // setString;
- // sp.set(key, value);
- }
-
- // setSearchParams(sp);
- },
- number: (value: number | number[]) => {
- if (Array.isArray(value)) {
- // setNumberArray;
- } else {
- // setNumber;
- }
- },
- boolean: (value: boolean | boolean[]) => {
- if (Array.isArray(value)) {
- // setBooleanArray;
- } else {
- // setBoolean;
- }
- },
- date: (value: Date | Date[]) => {
- if (Array.isArray(value)) {
- // setDateArray;
- } else {
- // setDate;
- }
- },
- });
-};
-
-// ----------------- useSearchParamsState -----------------
-
-export const useSearchParamsState = <
- Params extends UseSearchParamsStateParams = UseSearchParamsStateParams,
->(
- p: Params,
-): Readonly<{
- get: (key: string) => ReturnType;
- getAll: (key: string) => ReturnType;
- set: (key: string) => ReturnType;
-}> => {
- // Configure default values.
- const opts: Params = {
- removeDefaultValues: true,
- removeFalsyValues: true,
- sortKeys: true,
- instant: false,
- ...p,
- };
- // return [spObject as unknown as S, setState];
-
- return Object.freeze({
- get: (key) => {
- // Extract default value, taking first item if it's an array.
- const defaultValue = ((): SupportedValueTypes | undefined => {
- if (!p.defaultValues || !(key in p.defaultValues)) return undefined;
-
- const val = p.defaultValues[key];
- if (!val) return undefined;
-
- if (Array.isArray(val)) {
- return val.at(0);
- }
-
- return val;
- })();
-
- const value = p.searchParams.get(key) ?? defaultValue ?? undefined;
-
- return createGetter({
- value,
- });
- },
- getAll: (key) => {
- // Extract default value as an array, even if it's a single value.
- const defaultValue = ((): SupportedValueTypes[] | undefined => {
- if (!p.defaultValues || !(key in p.defaultValues)) return undefined;
-
- const val = p.defaultValues[key];
- if (!val) return undefined;
-
- if (Array.isArray(val)) {
- return val;
- }
-
- return [val];
- })();
-
- const value = p.searchParams.getAll(key);
-
- return createAllGetter({
- values:
- value.length > 0
- ? value
- : defaultValue && defaultValue.length > 0
- ? defaultValue
- : [],
- });
- },
- set: (key) => {
- // TODO: Implement
- console.log("WIP: Setting is not yet implemented.");
-
- return createSetter({ key });
- },
- });
-};
-
-// const s = useSearchParamsState({
-// searchParams: new URLSearchParams(),
-// setSearchParams: () => {},
-// });
-
-// s.get("page").asNumber().value();
-// s.get("perPage").asNumber().value();
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 8c9f61c..9e03490 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -1,3 +1,2 @@
export * from "./types";
-export * from "./hooks";
export * from "./utils";
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
index 4aad705..c9465ae 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -1,17 +1,11 @@
import type { ReadonlyURLSearchParams } from "next/navigation";
-export type SupportedValueTypes = number | string | boolean | Date | BigInt;
-export type SupportedArrayValueTypes =
- | number[]
- | string[]
- | boolean[]
- | Date[]
- | BigInt[];
-export type SupportedJointValueArrayTypes = SupportedValueTypes[];
-export type SupportedValues =
- | SupportedValueTypes
- | SupportedArrayValueTypes
- | SupportedJointValueArrayTypes;
+export type SupportedValueTypes = number | string | boolean | Date | bigint;
+export type SupportedValueArrayTypes = SupportedValueTypes[];
+export type SupportedValues = SupportedValueTypes | SupportedValueArrayTypes;
+
+// TODO: Replace string | string[] with SupportedValues
+export type SPHooksStateType = Record;
export interface UseSearchParamsStateBase {
searchParams: URLSearchParams | ReadonlyURLSearchParams;
diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts
index 0748672..462995f 100644
--- a/packages/react/src/utils.ts
+++ b/packages/react/src/utils.ts
@@ -1,17 +1,18 @@
-import { useEffect, useState, type DependencyList } from "react";
+import { useEffect } from "react";
import type { ReadonlyURLSearchParams } from "next/navigation";
-import { SupportedValues } from "./types";
+import { SPHooksStateType, SupportedValues } from "./types";
/*
* Conerts a URLSearchParams object to a plain object, with
* the values being either a string or an array of strings,
* depending on the number of values for a given key.
*/
-export const searchParamsToObject = (
+export const searchParamsToObject = (
sp: URLSearchParams | ReadonlyURLSearchParams,
-): Record => {
- const newObj: Record = {};
+ options?: { defaultValues?: Partial },
+): SPHooksStateType => {
+ const newObj: SPHooksStateType = {};
for (const key of Array.from(sp.keys())) {
const values = sp.getAll(key);
@@ -25,19 +26,30 @@ export const searchParamsToObject = (
}
}
+ if (options?.defaultValues) {
+ Object.entries(options.defaultValues).forEach(
+ ([key, value]: [string, string | string[]]) => {
+ if (typeof newObj[key] === "undefined") {
+ newObj[key] = value;
+ }
+ },
+ );
+ }
+
return newObj;
};
/*
* Converts a plain object to a URLSearchParams object.
*/
-export function stateToSearchParams>(
+export function stateToSearchParams(
state: S,
+ options?: { defaultValues?: Partial },
) {
const searchParams = new URLSearchParams();
Object.entries(state).forEach(([key, value]) =>
- mutSetValueToSearchParams(searchParams, key, value),
+ mutSetValueToSearchParams(searchParams, key, value, options),
);
return searchParams;
@@ -46,29 +58,35 @@ export function stateToSearchParams>(
/*
* Observe a state object and call setSearchParams when the state changes.
*/
-export function useObserveAndStore>(
+export function useObserveAndStore(
state: S,
setSearchParams: (newSearchParams: URLSearchParams) => void,
+ options?: { defaultValues?: Partial },
) {
useEffect(() => {
- setSearchParams(stateToSearchParams(state));
- }, Object.entries(state));
+ setSearchParams(stateToSearchParams(state, options));
+ }, [state]);
}
/*
* Accepts URLSearchParams as first parameter and a generic serialisable type as a second.
* Converts type to a string or an array of strings depending on the provided type and sets it in the URLSearchParams.
*/
-export function mutSetValueToSearchParams(
+export function mutSetValueToSearchParams(
sp: URLSearchParams,
key: string,
value: SupportedValues,
+ options?: { defaultValues?: Partial },
) {
if (Array.isArray(value)) {
value.forEach((v) => {
sp.append(key, encodeURIComponent(v.toString()));
});
} else {
+ if (options?.defaultValues && options.defaultValues[key] === value) {
+ return;
+ }
+
sp.set(key, encodeURIComponent(value.toString()));
}
}