From 793e7c896c9ee83aab676be42d612002a2ab00b1 Mon Sep 17 00:00:00 2001 From: danybeltran Date: Tue, 24 Jan 2023 09:06:48 -0600 Subject: [PATCH] feat(HttpReact): - Adds the HttpReact object, which can be used to make requests imperatively. It has a method for each HTTP verb, like 'get', 'post', etc. It also has a 'extend' method, similar to 'Axios.create' --- package.json | 2 +- src/components/index.tsx | 3 +- src/hooks/index.ts | 1 + src/hooks/others.ts | 10 +- src/hooks/use-fetch.ts | 11 +- src/index.ts | 21 ++-- src/internal/index.ts | 1 + src/utils/index.ts | 210 +------------------------------- src/utils/shared.ts | 253 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 285 insertions(+), 227 deletions(-) create mode 100644 src/utils/shared.ts diff --git a/package.json b/package.json index 3c6bb2e..4bb6bbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http-react", - "version": "2.6.6", + "version": "2.6.7", "description": "React hooks for data fetching", "main": "dist/index.js", "scripts": { diff --git a/src/components/index.tsx b/src/components/index.tsx index b9b50d1..d095200 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -1,3 +1,4 @@ +'use client' import * as React from 'react' import { useEffect, useState, Suspense } from 'react' @@ -12,7 +13,7 @@ import { import { FetchContextType } from '../types' -import { isDefined, serialize } from '../utils' +import { isDefined, serialize } from '../utils/shared' /** * This is a wrapper around `Suspense`. It will render `fallback` during the first render and then leave the rendering to `Suspense`. If you are not using SSR, you should continue using the `Suspense` component. diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 768c480..eb82f2d 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,3 +1,4 @@ +'use client' export { useFetch } from './use-fetch' export { diff --git a/src/hooks/others.ts b/src/hooks/others.ts index 087fd40..5cafe28 100644 --- a/src/hooks/others.ts +++ b/src/hooks/others.ts @@ -1,3 +1,4 @@ +'use client' import * as React from 'react' import { useEffect } from 'react' @@ -23,12 +24,9 @@ import { import { useFetch } from './use-fetch' -import { - createImperativeFetch, - isDefined, - isFunction, - serialize -} from '../utils' +import { createImperativeFetch } from '../utils' + +import { isDefined, isFunction, serialize } from '../utils/shared' import { ALLOWED_CONTEXT_KEYS, diff --git a/src/hooks/use-fetch.ts b/src/hooks/use-fetch.ts index 586c563..a568c8c 100644 --- a/src/hooks/use-fetch.ts +++ b/src/hooks/use-fetch.ts @@ -1,3 +1,4 @@ +'use client' import * as React from 'react' import { useState, useEffect } from 'react' @@ -42,19 +43,21 @@ import { import { createImperativeFetch, - createRequestFn, getMiliseconds, getTimePassed, + revalidate +} from '../utils' +import { + createRequestFn, hasBaseUrl, isDefined, isFunction, notNull, queue, - revalidate, serialize, setURLParams, windowExists -} from '../utils' +} from '../utils/shared' /** * Fetch hook @@ -1248,3 +1251,5 @@ useFetch.patch = createRequestFn('PATCH', '', {}) useFetch.purge = createRequestFn('PURGE', '', {}) useFetch.link = createRequestFn('LINK', '', {}) useFetch.unlink = createRequestFn('UNLINK', '', {}) + +useFetch.extend = createImperativeFetch diff --git a/src/index.ts b/src/index.ts index 8a505cc..3994146 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -'use client' /** * @license http-react * Copyright (c) Dany Beltran @@ -42,13 +41,17 @@ export { export { FetchConfig, SSRSuspense } from './components' -export { - gql, - setURLParams, - queryProvider, - mutateData, - revalidate, - hasBaseUrl -} from './utils' +export { gql, queryProvider, mutateData, revalidate } from './utils' export { defaultCache } from './internal' + +export { + HttpReact, + setURLParams, + hasBaseUrl, + isDefined, + isFormData, + isFunction, + serialize, + notNull +} from './utils/shared' diff --git a/src/internal/index.ts b/src/internal/index.ts index e17157d..8f5117a 100644 --- a/src/internal/index.ts +++ b/src/internal/index.ts @@ -1,3 +1,4 @@ +'use client' import { createContext, useContext } from 'react' import { CacheStoreType, FetchContextType } from '../types' diff --git a/src/utils/index.ts b/src/utils/index.ts index c83e5a6..601dfbd 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,10 +1,10 @@ +'use client' import * as React from 'react' import { useGql } from '../hooks/others' import { useFetch } from '../hooks/use-fetch' import { cacheForMutation, - defaultCache, previousConfig, requestInitialTimes, requestsProvider, @@ -12,22 +12,16 @@ import { valuesMemory } from '../internal' -import { - DEFAULT_RESOLVER, - METHODS, - UNITS_MILISECONDS_EQUIVALENTS -} from '../internal/constants' +import { UNITS_MILISECONDS_EQUIVALENTS } from '../internal/constants' import { CacheStoreType, FetchContextType, ImperativeFetch, - RequestWithBody, FetchInit, TimeSpan } from '../types' - -export const windowExists = typeof window !== 'undefined' +import { hasBaseUrl, isDefined, isFunction, queue, serialize } from './shared' export function getMiliseconds(v: TimeSpan): number { if (typeof v === 'number') return v @@ -43,96 +37,6 @@ export function getMiliseconds(v: TimeSpan): number { return amountNumber * UNITS_MILISECONDS_EQUIVALENTS[unit] } -export function notNull(target: any) { - return target !== null -} - -export function isDefined(target: any) { - return typeof target !== 'undefined' -} - -export function isFunction(target: any) { - return typeof target === 'function' -} - -export function hasBaseUrl(target: string) { - return target.startsWith('http://') || target.startsWith('https://') -} - -export function jsonCompare(a: any, b: any) { - return JSON.stringify(a) === JSON.stringify(b) -} - -export function serialize(input: any) { - return JSON.stringify(input) -} - -export const isFormData = (target: any) => { - if (typeof FormData !== 'undefined') { - return target instanceof FormData - } else return false -} - -export function queue(callback: any, time: number = 0) { - const tm = setTimeout(() => { - callback() - clearTimeout(tm) - }, time) - - return tm -} - -/** - * - * @param str The target string - * @param $params The params to parse in the url - * - * Params should be separated by `"/"`, (e.g. `"/api/[resource]/:id"`) - * - * URL search params will not be affected - */ -export function setURLParams(str: string = '', $params: any = {}) { - const hasQuery = str.includes('?') - - const queryString = - '?' + - str - .split('?') - .filter((_, i) => i > 0) - .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 - } - - return $params[segment.replace(/\[|\]/g, '')] - } else if (segment.startsWith(':')) { - const paramName = segment.split('').slice(1).join('') - if (!(paramName in $params)) { - console.warn( - `Param '${paramName}' does not exist in params configuration for '${str}'` - ) - return paramName - } - return $params[paramName] - } else { - return segment - } - }) - .join('/') + (hasQuery ? queryString : '') - ) -} - export function getTimePassed(key: any) { return ( Date.now() - @@ -140,110 +44,6 @@ export function getTimePassed(key: any) { ) } -/** - * Creates a new request function. This is for usage with fetcher and fetcher.extend - */ -export function createRequestFn( - method: string, - baseUrl: string, - $headers: any -): RequestWithBody { - return async function (url, init = {}) { - const { - default: def, - params = {}, - headers, - query = {}, - body, - formatBody, - resolver = DEFAULT_RESOLVER, - onResolve = () => {}, - onError = () => {} - } = init - - const rawUrl = setURLParams(url, params) - - const reqQueryString = Object.keys(query) - .map(q => [q, query[q]].join('=')) - .join('&') - - const reqConfig = { - method, - headers: { - 'Content-Type': 'application/json', - ...$headers, - ...headers - }, - body: canHaveBody(method as any) - ? isFunction(formatBody) - ? (formatBody as any)(body) - : body - : undefined - } - - let r = undefined as any - - const requestUrl = [ - baseUrl || '', - rawUrl, - url.includes('?') ? '&' : '?', - reqQueryString - ].join('') - - try { - const req = await fetch(requestUrl, { - ...init, - ...reqConfig - }) - r = req - - const data = await resolver(req) - if (req?.status >= 400) { - onError(true as any) - return { - res: req, - data: def, - error: true, - code: req?.status, - config: { - ...init, - url: `${baseUrl || ''}${rawUrl}`, - ...reqConfig, - query - } - } - } else { - onResolve(data, req) - return { - res: req, - data: data, - error: false, - code: req?.status, - config: { - ...init, - url: `${baseUrl || ''}${rawUrl}`, - ...reqConfig, - query - } - } - } - } catch (err) { - onError(err as any) - return { - res: r, - data: def, - error: true, - code: r?.status, - config: { - ...init, - url: requestUrl, - ...reqConfig - } - } - } - } as RequestWithBody -} - export const createImperativeFetch = (ctx: FetchContextType) => { const keys = [ 'GET', @@ -498,7 +298,3 @@ export function mutateData( } catch (err) {} } } - -export function canHaveBody(method: keyof typeof METHODS) { - return /(POST|PUT|DELETE|PATCH)/.test(method) -} diff --git a/src/utils/shared.ts b/src/utils/shared.ts new file mode 100644 index 0000000..cb35dbe --- /dev/null +++ b/src/utils/shared.ts @@ -0,0 +1,253 @@ +import { DEFAULT_RESOLVER, METHODS } from '../internal/constants' + +import { FetchContextType, ImperativeFetch, RequestWithBody } from '../types' + +export const windowExists = typeof window !== 'undefined' + +export function notNull(target: any) { + return target !== null +} + +export function isDefined(target: any) { + return typeof target !== 'undefined' +} + +export function isFunction(target: any) { + return typeof target === 'function' +} + +export function hasBaseUrl(target: string) { + return target.startsWith('http://') || target.startsWith('https://') +} + +export function jsonCompare(a: any, b: any) { + return JSON.stringify(a) === JSON.stringify(b) +} + +export function serialize(input: any) { + return JSON.stringify(input) +} + +export const isFormData = (target: any) => { + if (typeof FormData !== 'undefined') { + return target instanceof FormData + } else return false +} + +function canHaveBody(method: keyof typeof METHODS) { + return /(POST|PUT|DELETE|PATCH)/.test(method) +} + +export function queue(callback: any, time: number = 0) { + const tm = setTimeout(() => { + callback() + clearTimeout(tm) + }, time) + + return tm +} + +/** + * + * @param str The target string + * @param $params The params to parse in the url + * + * Params should be separated by `"/"`, (e.g. `"/api/[resource]/:id"`) + * + * URL search params will not be affected + */ +export function setURLParams(str: string = '', $params: any = {}) { + const hasQuery = str.includes('?') + + const queryString = + '?' + + str + .split('?') + .filter((_, i) => i > 0) + .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 + } + + return $params[segment.replace(/\[|\]/g, '')] + } else if (segment.startsWith(':')) { + const paramName = segment.split('').slice(1).join('') + if (!(paramName in $params)) { + console.warn( + `Param '${paramName}' does not exist in params configuration for '${str}'` + ) + return paramName + } + return $params[paramName] + } else { + return segment + } + }) + .join('/') + (hasQuery ? queryString : '') + ) +} + +/** + * Creates a new request function. This is for usage with fetcher and fetcher.extend + */ +export function createRequestFn( + method: string, + baseUrl: string, + $headers: any +): RequestWithBody { + return async function (url, init = {}) { + const { + default: def, + params = {}, + headers, + query = {}, + body, + formatBody, + resolver = DEFAULT_RESOLVER, + onResolve = () => {}, + onError = () => {} + } = init + + const rawUrl = setURLParams(url, params) + + const reqQueryString = Object.keys(query) + .map(q => [q, query[q]].join('=')) + .join('&') + + const reqConfig = { + method, + headers: { + 'Content-Type': 'application/json', + ...$headers, + ...headers + }, + body: canHaveBody(method as any) + ? isFunction(formatBody) + ? (formatBody as any)(body) + : body + : undefined + } + + let r = undefined as any + + const requestUrl = [ + baseUrl || '', + rawUrl, + url.includes('?') ? '&' : '?', + reqQueryString + ].join('') + + try { + const req = await fetch(requestUrl, { + ...init, + ...reqConfig + }) + r = req + + const data = await resolver(req) + if (req?.status >= 400) { + onError(true as any) + return { + res: req, + data: def, + error: true, + code: req?.status, + config: { + ...init, + url: `${baseUrl || ''}${rawUrl}`, + ...reqConfig, + query + } + } + } else { + onResolve(data, req) + return { + res: req, + data: data, + error: false, + code: req?.status, + config: { + ...init, + url: `${baseUrl || ''}${rawUrl}`, + ...reqConfig, + query + } + } + } + } catch (err) { + onError(err as any) + return { + res: r, + data: def, + error: true, + code: r?.status, + config: { + ...init, + url: requestUrl, + ...reqConfig + } + } + } + } as RequestWithBody +} + +const createImperativeFetch = (ctx: FetchContextType) => { + const keys = [ + 'GET', + 'DELETE', + 'HEAD', + 'OPTIONS', + 'POST', + 'PUT', + 'PATCH', + 'PURGE', + 'LINK', + 'UNLINK' + ] + + const { baseUrl } = ctx + + return Object.fromEntries( + new Map( + keys.map(k => [ + k.toLowerCase(), + (url, config = {}) => + (HttpReact as any)[k.toLowerCase()]( + hasBaseUrl(url) ? url : baseUrl + url, + { + ...ctx, + ...config + } + ) + ]) + ) + ) as ImperativeFetch +} + +const HttpReact = () => {} + +HttpReact.get = createRequestFn('GET', '', {}) +HttpReact.delete = createRequestFn('DELETE', '', {}) +HttpReact.head = createRequestFn('HEAD', '', {}) +HttpReact.options = createRequestFn('OPTIONS', '', {}) +HttpReact.post = createRequestFn('POST', '', {}) +HttpReact.put = createRequestFn('PUT', '', {}) +HttpReact.patch = createRequestFn('PATCH', '', {}) +HttpReact.purge = createRequestFn('PURGE', '', {}) +HttpReact.link = createRequestFn('LINK', '', {}) +HttpReact.unlink = createRequestFn('UNLINK', '', {}) + +HttpReact.extend = createImperativeFetch + +export { HttpReact }