From a9fdcb37649f1ad415a126f09d1cee4d579e79c1 Mon Sep 17 00:00:00 2001 From: danybeltran Date: Mon, 10 Nov 2025 02:38:07 -0600 Subject: [PATCH] features(params): Adds static typing for params specified in URL --- package.json | 2 +- src/hooks/others.ts | 1 + src/hooks/use-fetch.ts | 17 ++++++++--- src/types/index.ts | 50 ++++++++++++++++++++++++++++++++ src/utils/index.ts | 65 +++++++++++++++++++++++------------------- src/utils/shared.ts | 61 +++++++++++++++++++++++---------------- 6 files changed, 138 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index bc5bded..1c4a53c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http-react", - "version": "3.8.7", + "version": "3.8.8", "description": "React hooks for data fetching", "main": "dist/index.js", "scripts": { diff --git a/src/hooks/others.ts b/src/hooks/others.ts index a319c3b..701a08b 100644 --- a/src/hooks/others.ts +++ b/src/hooks/others.ts @@ -699,6 +699,7 @@ export function useServerAction any>( let mockServerActionId = config?.id ?? getMockServerActionId(action) const $action = useFetch(mockServerActionId, { + params: {}, fetcher: async function proxied(_, config) { const actionParam = actionForms.get(mockServerActionId) ?? config?.params $action.resetError() diff --git a/src/hooks/use-fetch.ts b/src/hooks/use-fetch.ts index 7199d4f..58120b1 100644 --- a/src/hooks/use-fetch.ts +++ b/src/hooks/use-fetch.ts @@ -41,6 +41,8 @@ import { FetchContextType, HTTP_METHODS, ImperativeFetch, + StaticFetchConfig, + StaticFetchConfigNoUrl, TimeSpan } from '../types' @@ -82,9 +84,16 @@ const temporaryFormData = new Map() /** * Fetch hook */ -export function useFetch( - init: FetchConfigType | string | Request, - options?: FetchConfigTypeNoUrl +export function useFetch< + FetchDataType = any, + TransformData = any, + UrlType extends string = string +>( + init: + | StaticFetchConfig + | UrlType + | Request, + options?: StaticFetchConfigNoUrl ) { const $ctx = useHRFContext() @@ -129,7 +138,7 @@ export function useFetch( ...options, // @ts-expect-error id: init?.id ?? init?.key - } as Required>) + } as Required>) const { onOnline = ctx.onOnline, diff --git a/src/types/index.ts b/src/types/index.ts index 99c7e37..001ee30 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -371,3 +371,53 @@ export type FetchInit = FetchConfigType< FDT, TransformData >; + +// types related to params parsing + +/** Helper type to extract the parameter name from a segment like :id or {id} or [id] */ +export type ExtractParam = + Segment extends `[${infer Name}]` + ? Name // [name] + : Segment extends `:${infer Name}` + ? Name // :name + : Segment extends `{${infer Name}}` + ? Name // {name} + : never; + +type CleanPath = Path extends `/${infer Rest}` + ? CleanPath + : Path extends `${infer Rest}/` + ? CleanPath + : Path extends `${infer Base}?${any}` + ? CleanPath + : Path; + +type ParsePathParams = + Path extends `${infer Segment}/${infer Rest}` + ? (ExtractParam extends never + ? {} + : { [K in ExtractParam]: string | number }) & + ParsePathParams + : ExtractParam extends never + ? {} + : { [K in ExtractParam]: string | number }; + +export type PathParams = ParsePathParams>; + +export type StaticParams = + PathParams extends Record + ? { params?: any } + : { params: PathParams }; + +export type StaticFetchConfig = Omit< + FetchConfigType, + "url" | "params" +> & { + url?: U; +} & StaticParams; + +export type StaticFetchConfigNoUrl = Omit< + FetchConfigTypeNoUrl, + "params" +> & + StaticParams; diff --git a/src/utils/index.ts b/src/utils/index.ts index 828a217..822bd7c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,7 +11,7 @@ import { requestInitialTimes, requestsProvider, runningMutate, - valuesMemory, + valuesMemory } from "../internal"; import { UNITS_MILISECONDS_EQUIVALENTS } from "../internal/constants"; @@ -24,6 +24,8 @@ import { TimeSpan, FetchConfigTypeNoUrl, FetchConfigType, + StaticFetchConfig, + StaticFetchConfigNoUrl } from "../types"; import { gql, @@ -32,7 +34,7 @@ import { isFunction, queue, serialize, - windowExists, + windowExists } from "./shared"; export function getMiliseconds(v: TimeSpan): number { @@ -67,7 +69,7 @@ export const createImperativeFetch = (ctx: FetchContextType) => { "PATCH", "PURGE", "LINK", - "UNLINK", + "UNLINK" ]; const { baseUrl } = ctx; @@ -75,20 +77,20 @@ export const createImperativeFetch = (ctx: FetchContextType) => { return { ...Object.fromEntries( new Map( - keys.map((k) => [ + keys.map(k => [ k.toLowerCase(), (url, config = {}) => (useFetch as any)[k.toLowerCase()]( hasBaseUrl(url) ? url : baseUrl + url, { ...ctx, - ...config, + ...config } - ), + ) ]) ) ), - config: ctx, + config: ctx } as ImperativeFetch; }; @@ -101,7 +103,7 @@ export const useIsomorphicLayoutEffect = windowExists */ export function revalidate(id: any | any[], __reval__ = true) { if (Array.isArray(id)) { - id.map((reqId) => { + id.map(reqId => { if (isDefined(reqId)) { const key = serialize(reqId); @@ -118,7 +120,7 @@ export function revalidate(id: any | any[], __reval__ = true) { queue(() => { requestsProvider.emit(key, { loading: true, - error: false, + error: false }); }); } @@ -142,7 +144,7 @@ export function revalidate(id: any | any[], __reval__ = true) { queue(() => { requestsProvider.emit(key, { loading: true, - error: false, + error: false }); }); } @@ -157,17 +159,17 @@ export function revalidateKey(key: any) { export function cancelRequest(id: any | any[]) { if (Array.isArray(id)) { - id.map((reqId) => { + id.map(reqId => { if (isDefined(reqId)) { const key = serialize({ - idString: serialize(reqId), + idString: serialize(reqId) }); if (isPending(key)) { revalidate(reqId, false); queue(() => { requestsProvider.emit(key, { loading: false, - error: false, + error: false }); }); } @@ -176,14 +178,14 @@ export function cancelRequest(id: any | any[]) { } else { if (isDefined(id)) { const key = serialize({ - idString: serialize(id), + idString: serialize(id) }); if (isPending(key)) { revalidate(id, false); queue(() => { requestsProvider.emit(key, { loading: false, - error: false, + error: false }); }); } @@ -249,7 +251,7 @@ export function queryProvider( const queryVariables = { ...thisDefaults?.variables, - ...(otherConfig as any)?.variables, + ...(otherConfig as any)?.variables }; const { config = {} } = providerConfig || {}; @@ -276,7 +278,7 @@ export function queryProvider( headers: { ...others?.headers, ...thisDefaults?.headers, - ...otherConfig?.headers, + ...otherConfig?.headers }, ...{ __fromProvider: true }, default: { @@ -287,15 +289,15 @@ export function queryProvider( * 'value' property (when using the `gql` function) */ // @ts-ignore - otherConfig?.default) as R[P]["value"], + otherConfig?.default) as R[P]["value"] }, - variables: queryVariables, + variables: queryVariables }); const thisData = useMemo( () => ({ ...g?.data, - variables: queryVariables, + variables: queryVariables }), [serialize({ data: g?.data, queryVariables })] ); @@ -304,9 +306,9 @@ export function queryProvider( ...g, config: { ...g?.config, - config: undefined, + config: undefined }, - data: thisData, + data: thisData } as Omit & { data: { data: QuerysType[P] extends ReturnType @@ -338,7 +340,7 @@ export function mutateData( requestsProvider.emit(key, { data: newVal, isMutating: true, - requestCallId, + requestCallId }); if (_revalidate) { previousConfig.set(key, undefined); @@ -353,7 +355,7 @@ export function mutateData( requestsProvider.emit(key, { requestCallId, isMutating: true, - data: v, + data: v }); if (_revalidate) { previousConfig.set(key, undefined); @@ -368,11 +370,16 @@ export function mutateData( } } -export function fetchOptions( - init: string | FetchConfigType, - options?: FetchConfigTypeNoUrl -) { +export function fetchOptions< + T = any, + TransformData = any, + UrlType extends string = string +>( + init: UrlType | StaticFetchConfig, + options?: Partial> +): StaticFetchConfig { + // Changed return type return ( typeof init === "string" ? { url: init, ...options } : init - ) as FetchConfigType; + ) as StaticFetchConfig; // Changed cast } diff --git a/src/utils/shared.ts b/src/utils/shared.ts index 0284786..a1fa1f5 100644 --- a/src/utils/shared.ts +++ b/src/utils/shared.ts @@ -1,6 +1,11 @@ import { DEFAULT_RESOLVER, METHODS } from "../internal/constants"; -import { FetchContextType, ImperativeFetch, RequestWithBody } from "../types"; +import { + FetchContextType, + ImperativeFetch, + PathParams, + RequestWithBody, +} from "../types"; export const windowExists = typeof window !== "undefined"; @@ -87,45 +92,53 @@ export function queue(callback: any, time: number = 0) { * * URL search params will not be affected */ +export function setURLParams( + str: UrlType, + $params: PathParams extends Record + ? any + : PathParams +): string; + export function setURLParams(str: string = "", $params: any = {}) { const hasQuery = str.includes("?"); - const queryString = - "?" + - str - .split("?") - .filter((_, i) => i > 0) - .join("?"); + const queryString = hasQuery ? "?" + str.split("?").slice(1).join("?") : ""; return ( str .split("/") .map(($segment) => { - const [segment] = $segment.split("?"); - if (segment.startsWith("[") && segment.endsWith("]")) { - const paramName = segment.replace(/\[|\]/g, ""); - if (!(paramName in $params)) { - console.warn( - `Param '${paramName}' does not exist in params configuration for '${str}'` - ); - return paramName; - } + const [segmentPath] = $segment.split("?"); + + let paramName = null; + let isParam = false; + + if (segmentPath.startsWith("[") && segmentPath.endsWith("]")) { + paramName = segmentPath.slice(1, -1); + isParam = true; + } else if (segmentPath.startsWith(":")) { + paramName = segmentPath.slice(1); + isParam = true; + } else if (segmentPath.startsWith("{") && segmentPath.endsWith("}")) { + paramName = segmentPath.slice(1, -1); + isParam = true; + } - return $params[segment.replace(/\[|\]/g, "")]; - } else if (segment.startsWith(":")) { - const paramName = segment.split("").slice(1).join(""); + if (isParam && paramName) { if (!(paramName in $params)) { console.warn( - `Param '${paramName}' does not exist in params configuration for '${str}'` + `Param '${paramName}' does not exist in params configuration for URL '${str}'` ); - return paramName; + + return segmentPath; } + return $params[paramName]; - } else { - return segment; } + + return $segment; }) - .join("/") + (hasQuery ? queryString : "") + .join("/") + queryString ); }