react-qc-v
TypeScript icon, indicating that this package has built-in type declarations

1.5.6 • Public • Published

Welcome to react-qc 👋

Lightweight @tanstack/react-query wrapper that makes hooks reusable, provides error/loading, and more...

Documentation Maintenance

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>

Table of Contents

Installation for @tanstack/react-query v5

npm install react-qc-v

Requirements

  • react: ^18
  • react-dom: ^18
  • @tanstack/react-query: v5

Installation for @tanstack/react-query v4

npm install react-qc-iv

Requirements

  • react: ^16.8.0 || ^17 || ^18
  • react-dom: ^16.8.0 || ^17 || ^18
  • @tanstack/react-query: v4

Installation for react-query v3

npm install react-qc-iii

Requirements

  • react: ^16.8.0 || ^17 || ^18
  • react-dom: ^16.8.0 || ^17 || ^18
  • react-query: v3

API Reference

  • 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

Define new query

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());
  }
});

Use the query

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>
  );
}

Set custom loading/error

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>
  );
}

Add provider for default loading/error

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>
  );
}

Add retry button

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>
  );
}

Define custom variables

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());
  }
});

Pass variables

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: Syntactic sugar

optional path prop as variables[0] and body prop as variables[1]

or

optional path prop as variables[0] and variables as variables[1]

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>
  );
}

Custom data function

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>
  );
}

Pagination

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,
});

Use infinite query

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>
  );
}

Use infinite query with custom data function

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>
  );
}

Advanced: add extensions

🚨 IMPORTANT
for extensions: You need to define wrapUseQueryWithExtensions, or wrapUseInfiniteQueryWithExtensions when defining your query

import { 
 ...
 wrapUseQueryWithExtensions,
 wrapUseInfiniteQueryWithExtensions,
 ...
} from 'react-qc-iv';
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>
  );
}

Advanced: use extensions with default keyFn

pass a callback function in place of a variable and it will be called with extensions to create that specific variable

<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

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

Advanced: use extensions with custom keyFn

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);

Extra: default key fn

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

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;
};

Extra: by default error/loading apply only to first page

take the previous 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

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>
  );
}

Extra: how to pass react query options?

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

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>
  );
}

Extra: how to disable default error/loading behavior?

use props like these hasLoading={false} throwOnError={false} useErrorBoundary={false} to disable default error/loading behavior

License

MIT

Package Sidebar

Install

npm i react-qc-v

Weekly Downloads

5

Version

1.5.6

License

MIT

Unpacked Size

73.7 kB

Total Files

14

Last publish

Collaborators

  • muhamadalfaifi