react-qc-v
Version:
Lightweight @tanstack/react-query wrapper that provides error/loading, and more...
664 lines (532 loc) • 17.3 kB
Markdown
Lightweight @tanstack/react-query wrapper that makes hooks reusable, provides error/loading, and more...
[](#table-of-contents)
[](https://github.com/MuhamadAlfaifi/react-qc/graphs/commit-activity)
```javascript
const Get = wrapUseQuery<[string, Record<string, any>] | [string]>({
// no need to define queryKey now!
queryFn: async ({ queryKey: [path, search] }) => {
const url = [path];
search && url.push(
new URLSearchParams(Object.entries(search)).toString()
);
return await fetch(url.join('?')).then((res) => res.json());
}
});
type TName = {
title: string,
first: string,
last: string,
}
type Response = {
results: {
name: TName,
...
}[]
}
const names = (data: Response) => data.results.map((item) => item.name);
// reusable hook Get.use(): info.data is TName[] | undefined
const info = Get.use(['https://randomuser.me/api', { results: 10 }], { select: names });
// reusable component <Get />: data is TName[]
<Catch error={<p>an error occured!</p>}>
<Get path="https://randomuser.me/api" variables={{ results: 10 }} loading={<p>loading...</p>} select={names}>
{(data) => ( // data is TName[]
<ul>
{data.map((name, id) =>
<li key={id}>{name.first} - {name.last}</li>
)}
</ul>
)}
</Get>
</Catch>
```
- Installation
- [Installation for @tanstack/react-query v5](
- [Installation for @tanstack/react-query v4](
- [Installation for react-query v3](
- API Reference
- Tutorial
- [Define new query](
- [Use the query](
- [Set custom loading/error](
- [Add provider for default loading/error](
- [Add retry button](
- [Define custom variables](
- [Pass variables](
- [Optional: Syntactic sugar](
- [Custom data function](
- [Pagination](
- [Use infinite query](
- [Use infinite query with custom data function](
- [Advanced: add extensions](
- [Advanced: use extensions with default keyFn](
- [Advanced: use extensions with custom keyFn](
- Extra
- [Extra: default key fn](
- [Extra: by default error/loading apply only to first page](
- [Extra: how to pass react query options?](
- [Extra: how to disable default error/loading behavior?](
- License
```bash
npm install react-qc-v
```
- react: ^18
- react-dom: ^18
- @tanstack/react-query: v5
```bash
npm install react-qc-iv
```
- react: ^16.8.0 || ^17 || ^18
- react-dom: ^16.8.0 || ^17 || ^18
- @tanstack/react-query: v4
```bash
npm install react-qc-iii
```
- react: ^16.8.0 || ^17 || ^18
- react-dom: ^16.8.0 || ^17 || ^18
- react-query: v3
- QcProvider
- Props
- loading (optional) - default loading component
- error (optional) - default error component
- QcExtensionsProvider
- Props
- extensions (required) - extensions or hook that returns extensions
- wrapUseQuery
- Parameters
- options (required) - useQuery options
- keyFn (optional) - custom keyFn
- Returns
- Component - enhanced useQuery component
- wrapUseInfiniteQuery
- Parameters
- options (required) - useInfiniteQuery options
- keyFn (optional) - custom keyFn
- Returns
- Component - enhanced useInfiniteQuery component
- wrapUseQueryWithExtensions
- Parameters
- options (required) - useQuery options
- keyFn (optional) - custom keyFn
- Returns
- Component - enhanced useQuery component
- wrapUseInfiniteQueryWithExtensions
- Parameters
- options (required) - useInfiniteQuery options
- keyFn (optional) - custom keyFn
- Returns
- Component - enhanced useInfiniteQuery component
- Catch
- Props
- error (optional) - custom error component or null
- s
- Parameters
- strings (required) - template literal strings
- values (optional) - template literal values
- Returns
- string - string with substituted values from extensions.searchParams
```tsx
import { wrapUseQuery } from 'react-qc-iv';
export const Get = wrapUseQuery({
queryKey: ['users'],
queryFn: async () => {
return await fetch('https://randomuser.me/api').then((res) => res.json());
}
});
```
```tsx
import { Get } from 'path/to/Get';
// use `Get` as a component
function MyComponent() {
return (
<Get>
{(data) => (
<div>
{JSON.stringify(data)}
</div>
)}
</Get>
);
}
// use `Get` as a hook
function MyComponent() {
const { data } = Get.use();
return (
<div>
{JSON.stringify(data)}
</div>
);
}
```
```tsx
import { Catch } from 'react-qc-iv';
import { Get } from 'path/to/Get';
// use `Get` as a component
function MyComponent() {
return (
<Catch error={<div>an error occured!</div>}>
<Get loading={'loading...'}>
{(data) => (
<div>
{JSON.stringify(data)}
</div>
)}
</Get>
</Catch>
);
}
```
```tsx
import { QcProvider, Catch } from 'react-qc-iv';
import { Get } from 'path/to/Get';
function App() {
return (
<QcProvider loading={'loading...'} error={<div>an error occured!</div>}>
<MyComponent />
</QcProvider>
);
}
// use `Get` as a component with provided loading/error
function MyComponent() {
return (
<Catch>
<Get>
{(data) => (
<div>
{JSON.stringify(data)}
</div>
)}
</Get>
</Catch>
);
}
```
```tsx
import { Catch } from 'react-qc-iv';
import { Get } from 'path/to/Get';
function App() {
return (
<QcProvider loading={'loading...'} error={({ retry }) => <button onClick={retry}>retry</button>}>
<MyComponent />
</QcProvider>
);
}
```
```tsx
import { wrapUseQuery } from 'react-qc-iv';
import { useQuery } from '@tanstack/react-query';
export const Get = wrapUseQuery<[string, Record<string, any> | undefined]>({
queryFn: async ({ queryKey: [path, search = {}] }) => {
const searchParams = new URLSearchParams(Object.entries(search));
return await fetch(path + '?' + searchParams.toString()).then((res) => res.json());
}
});
```
```tsx
import { Get } from 'path/to/Get';
// use `Get` as a component
function MyComponent() {
return (
<Get variables={['https://randomuser.me/api', { results: 10 }]}> {/* variables prop type here is the generic parameter associated with Get */}
{(data) => (
<div>
{JSON.stringify(data)}
</div>
)}
</Get>
);
}
// use `Get` as a hook
function MyComponent() {
const { data } = Get.use(['https://randomuser.me/api', { results: 10 }]); // variables prop type here is the generic parameter associated with Get
return (
<div>
{JSON.stringify(data)}
</div>
);
}
```
optional path prop as variables[0] and body prop as variables[1]
or
optional path prop as variables[0] and variables as variables[1]
```tsx
import { Get } from 'path/to/Get';
// use `Get` as a component
function MyComponent() {
return (
<Get path="https://randomuser.me/api" variables={{ results: 10 }}>
{(data) => (
<div>
{JSON.stringify(data)}
</div>
)}
</Get>
);
}
```
```tsx
import { Get } from 'path/to/Get';
type TName = { title: string, first: string, last: string }
type Response = {
results: {
name: TName
}[]
}
const names = (data: Response) => data.results.map((item) => item.name) || [];
// pass select function prop
function MyComponent() {
return (
<Get path="https://randomuser.me/api" variables={{ results: '10' }} select={names}>
{(data, query) => ( // data is TName[] and query.data is TName[] | undefined
<ul>
{data.map((name, index) => (
<li key={index}>{name.first} {name.last}</li>
))}
</ul>
)}
</Get>
);
}
// pass data function parameter
function MyComponent() {
const { data } = Get.use(['https://randomuser.me/api', { results: '10' }], { select: names }); // data is TName[] | undefined
return (
<ul>
{data.map((name, index) => (
<li key={index}>{name.first} {name.last}</li>
))}
</ul>
);
}
```
```tsx
import { wrapUseInfiniteQuery } from 'react-qc-iv';
export const Paginate = wrapUseInfiniteQuery<[string, Record<string, any>]>({
queryFn: async ({ queryKey: [url, parameters], pageParam, meta: { initialPageParam = 0 } = {} }) => {
const search = new URLSearchParams();
for (const key in parameters) {
search.set(key, String(parameters[key]));
}
const page = typeof pageParam === 'number' ? pageParam : initialPageParam;
search.set('page', page);
return await fetch(url + '?' + search.toString()).then((res) => res.json());
},
getNextPageParam: (lastPage) => lastPage.info.page + 1,
});
```
```tsx
import { Paginate } from 'path/to/Paginate';
// use `Paginate` as a component
function MyComponent() {
return (
<Paginate path="https://randomuser.me/api" variables={{ results: 10 }}>
{(data, { fetchNextPage, hasNextPage }) => (
<div>
<div>{JSON.stringify(data)}</div>
<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>fetch next page</button>
</div>
)}
</Paginate>
);
}
// use `Paginate` as a hook
function MyComponent() {
const { data, fetchNextPage, hasNextPage } = Paginate.use(['https://randomuser.me/api', { results: 10 }]);
return (
<div>
<div>{JSON.stringify(data)}</div>
<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>fetch next page</button>
</div>
);
}
```
```tsx
import { type InfiniteData } from '@tanstack/react-query';
import { Paginate } from 'path/to/Paginate';
type TName = { title: string, first: string, last: string }
type Response = {
results: {
name: TName
}[]
}
const names = (data: InfiniteData<Response>) => data.pages.flatMap(page => data.results.map((item) => item.name));
// pass data function prop
function MyComponent() {
return (
<Paginate path="https://randomuser.me/api" variables={{ results: 10 }} select={names}>
{(data, { fetchNextPage, hasNextPage }) => (
<div>
<ul>
{data.map((name, index) =>
<li key={index}>{name.first} {name.last}</li>
)}
</ul>
<li><button onClick={() => fetchNextPage()} disabled={!hasNextPage}>fetch next page</button></li>
</div>
)}
</Paginate>
);
}
// pass data function parameter
function MyComponent() {
const { data, fetchNextPage, hasNextPage } = Paginate.use(['https://randomuser.me/api', { results: 10 }], { select: names });
return (
<div>
<ul>
{data.map((name, index) => (
<li key={index}>{name.first} {name.last}</li>
))}
</ul>
<li><button onClick={() => fetchNextPage()} disabled={!hasNextPage}>fetch next page</button></li>
</div>
);
}
```
> 🚨 IMPORTANT
> for extensions: You need to define
> wrapUseQueryWithExtensions, or
> wrapUseInfiniteQueryWithExtensions when defining your query
> ```tsx
> import {
> ...
> wrapUseQueryWithExtensions,
> wrapUseInfiniteQueryWithExtensions,
> ...
> } from 'react-qc-iv';
> ```
```tsx
import { QcExtensionsProvider } from 'react-qc-iv';
import { useSearchParams, useParams } from 'react-router-dom';
function useExtensions() {
const params = useParams();
const [searchParams] = useSearchParams();
return { params, searchParams };
}
function App() {
const extensions = useExtensions();
return (
<QcExtensionsProvider extensions={extensions}> // Alternatively, pass hook directly like extensions={useExtensions} for similar result
<MyComponent />
</QcExtensionsProvider>
);
}
```
pass a callback function in place of a variable and it will be called with extensions to create that specific variable
```tsx
<Get variables={[(extensions) => `/path/${extensions.searchParams.get('id')}`, { ...stuff }]} ...>...</Get>
```
for building strings using react router like extensions.searchParams.get('id'), you can use `s` template literal tag for substituting searchParams values in the string and Optionally, you can add fallback with ! for example s`/path/${'id!0'}` will fallback to 0 if id is not found in searchParams
```tsx
import { s } from 'react-qc-iv';
<Get path={s`/path/${'id!0'}`} variables={{ ...stuff }} ...>...</Get>
```
since the first variable is a callback function the default keyFn will call it for you with extensions as the first parameter
```tsx
import { wrapUseQueryWithExtensions } from 'react-qc-iv';
import type { QueryKey } from '@tanstack/react-query';
type TKeyFn = (variables: unknown[], extensions: { searchParams: URLSearchParams, params: Record<string, any> }) => QueryKey;
const customKeyFn: TKeyFn = (variables, extensions) => {
const [path, body] = variables;
const { params, searchParams } = extensions;
return [path, { body, params, searchParams: searchParams.toString() }];
}
export const Get = wrapUseQueryWithExtensions<[string, Record<string, any>]>({
queryFn: async ({ queryKey: [url, { body, params, searchParams }] }) => {
...
},
}, customKeyFn);
```
You can pass callbacks that generate the queryKey and the default keyFn will call them for you with optional extensions as the first parameter
here is the imlementation of the default keyFn
```tsx
import { TVariableFn } from './types';
import { QueryKey } from '@tanstack/react-query';
export const defaultKeyFn = <T extends TVariableFn<unknown> | TVariableFn<unknown>[] | unknown[], Extensions = never>(variables: T, extensions: Extensions): QueryKey => {
if (typeof variables === 'function') {
return variables(extensions) as unknown as QueryKey;
}
return variables.map((variable) => {
if (typeof variable === 'function') {
return variable(extensions);
}
return variable;
}) as unknown as QueryKey;
};
```
take the previous <Paginate /> example, the loading/error in case promise pending/rejected will not be shown on 2nd page and so on
if first page is already rendered and next page rejected you should handle the error manually using `isFetchingNextPage` and `error` properties
```tsx
import { Paginate } from 'path/to/Paginate';
// use `Paginate` as a component
function MyComponent() {
return (
<Catch error={<p>first page rejected!</p>}>
<Paginate path="https://randomuser.me/api" loading={<p>first page spinner!</p>} variables={{ results: 10 }}>
{(data, { fetchNextPage, hasNextPage, isFetchingNextPage, error }) => (
<div>
<div>{JSON.stringify(data)}</div>
{hasNextPage
? <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>{error ? 'retry' : 'fetch'} next page</button>
: <p>no more results.</p>}
</div>
)}
</Paginate>
</Catch>
);
}
```
you can pass refetchInterval or any other react query options to the query by passing it as 2nd parameter to the hook, or directly pass refetchInterval as a prop to the component
```tsx
import { Get } from 'path/to/Get';
// use `Get` as a component
function MyComponent() {
return (
<Get path="https://randomuser.me/api" variables={{ results: 10 }} refetchInterval={5000}>
{(data) => (
<div>
{JSON.stringify(data)}
</div>
)}
</Get>
);
}
// use `Get` as a hook
function MyComponent() {
const { data } = Get.use(['https://randomuser.me/api', { results: 10 }], { refetchInterval: 5000 });
return (
<div>
{JSON.stringify(data)}
</div>
);
}
```
use props like these `hasLoading={false} throwOnError={false} useErrorBoundary={false}` to disable default error/loading behavior
MIT