0.9.5 • Published 8 months ago

@k4l3b4/query-adapters v0.9.5

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

Query Adapters

Overview

Query Adapters is a flexible TypeScript library that provides advanced data fetching components for React applications, built on top of React Query.

Why?

I have been working with React Query for a while now and I wanted to create a library that would make my life easier and provide a more efficient way to fetch data from the server, i was using it in my projects for a while now and thought i would just make it into a package and share it with the community.

Installation

npm install @k4l3b4/query-adapters@latest

using other package managers

pnpm add @k4l3b4/query-adapters@latest
yarn add @k4l3b4/query-adapters // yishhhh 🙄

Dependencies

  • React >= v16
  • React Query >= v5
  • TypeScript >= v4

Mental dependency

Components

1. DataFetcher

A generic data fetching component with simplified query management.

Usage Example with query function

import { DataFetcher } from '@k4l3b4/query-adapters';
import { fetchUserDetails } from './api';
import UserProfile from './UserProfile';

<DataFetcher
  queryKey={['user', userId]}
  queryFn={() => fetchUserDetails(userId)}
>
  {({ data, error, isLoading, status }) => (
    {isLoading ? <Spinner /> : <UserProfile user={data?.user} />}
  )}
</DataFetcher>

Usage Example with url string

<DataFetcher<TUser, TError> // returned data and error will be of type TUser and TError respectively
  queryKey={['user', userId]}
  url={`/api/user/${userId}`} // don't pass query params here, use queryParams prop instead
>
  {({ data, error, isLoading, status }) => (
    {isLoading ? <Spinner /> : <UserProfile user={data?.user} />}
  )}
</DataFetcher>

The reason you can't pass query params directly in the url string is because if you later on decide to add query params to the queryParams prop, the produced URL will be incorrect: api/users/filter?{the_query_param_passed_in=the_url_prop}?{the_query_params_passed_in=the_queryParams_prop}

Notice the second query param in the URL having a ? before it, this is because the queryParams prop is an object, and we need to concatenate it with the url string and since the queryParams prop has no knowledge of the url string, we are just assuming that the url string is correct and appending the queryParams to it.

Usage Example with url string and queryParams⭐

<DataFetcher<TUser[], TError> // returned data and error will be of type TUser[] and TError respectively
  queryKey={['user']} // pass the query params that need to be tracked as queryKeys
  url={`/api/users/filter`}
  queryParams={{active: true, sort_by: "id", sort: "desc", page: 2}} 
  // this will produce a url like /api/users/filter?active=true&sort_by=id&sort=desc&page=2
  // cool right?😁
>
  {({ data, error, isLoading, status }) => (
    {isLoading ? <Spinner /> : data?.map(user => <UserProfile user={user} />)}
  )}
</DataFetcher>

2. InfiniteDataFetcher

A powerful, customizable component for implementing infinite scrolling and pagination.

Features

  • Automatic or manual page loading
  • Flexible data fetching strategy
  • Intersection Observer for scroll-based loading
  • Customizable and swappable states. (loading, error & no more data components)

Basic Usage Example

NOTE: please read tanstack's react-query documentation for more information on how to use this component.

<InfiniteDataFetcher<TUser[], TError> // returned data ie page?.users will be of type TUser[] and error will be of type TError.
  queryKey={['users']}
  queryFn={({ pageParam }) => fetchUsers(pageParam)}
  options={{
    getNextPageParam: (lastPage, allPages) => lastPage.nextPage
  }}
>
  {({
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  }) => (
    <>
      {data?.map((page, pageIndex) => (
        // returns an array of pages, each page contains an array of items
          <React.Fragment key={pageIndex}>
              {page?.users.map((post) => (
                  <UserCard key={item.id} user={item} />
              ))}
          </React.Fragment>
      ))}
      {hasNextPage && (
        <button onClick={fetchNextPage} disabled={isFetchingNextPage}>
          Load More
        </button>
      )}
    </>
  )}
</InfiniteDataFetcher>

Advanced Examples

1. Custom Loading and No More Data Components
<InfiniteDataFetcher
  queryKey={['products']}
  queryFn={({ pageParam }) => fetchProducts(pageParam)}
  loadingComponent={<CustomSpinner />}
  noMoreDataComponent={<p>No more products to show</p>}
  triggerComponent={
    <button className="custom-load-more">
      Fetch More Products
    </button>
  }
  enableManualFetch
>
  {({ data, fetchNextPage, hasNextPage }) => (
    <div>
      {data?.map((page, pageIndex) => (
          <React.Fragment key={pageIndex}>
              {page?.product.map((post) => (
                  <ProductCard key={product.id} product={product} />
              ))}
          </React.Fragment>
      ))}
      {hasNextPage && <button onClick={fetchNextPage}>Load More</button>}
    </div>
  )}
</InfiniteDataFetcher>
2. Custom Intersection Observer Logic
import React, { useEffect, useRef } from 'react';
import { InfiniteDataFetcher } from '@k4l3b4/query-adapters';
import { fetchBlogPosts } from './api';
import BlogPostCard from './BlogPostCard';

interface IntersectionObserverProps {
  onIntersect: () => void;
  hasNextPage: boolean;
}



const ProductList = () => {
  return (
    <InfiniteDataFetcher
      queryKey={['blog-posts']}
      queryFn={({ pageParam }) => fetchBlogPosts(pageParam)}
      options={{
        getNextPageParam: (lastPage) => lastPage.nextCursor,
      }}
      enableManualFetch // Disable auto-fetching since we're implementing custom logic
    >
      {({ data, fetchNextPage, hasNextPage }) => (
        <div>
          {data?.map((page, pageIndex) => (
            <React.Fragment key={pageIndex}>
              {page?.posts.map((post) => (
                <BlogPostCard key={post.id} post={post} />
              ))}
            </React.Fragment>
          ))}

          {hasNextPage && (
            <IntersectionObserverComponent
              onIntersect={fetchNextPage}
              hasNextPage={hasNextPage}
            />
          )}
        </div>
      )}
    </InfiniteDataFetcher>
  );
};

export default ProductList;



// handles intersection observer logic
const IntersectionObserverComponent: React.FC<IntersectionObserverProps> = ({ onIntersect, hasNextPage }) => {
  const observerRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (!observerRef.current || !hasNextPage) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          onIntersect();
        }
      },
      { threshold: 1.0 }
    );

    observer.observe(observerRef.current);

    return () => observer.disconnect();
  }, [onIntersect, hasNextPage]);

  return (
    <div
      ref={observerRef}
      style={{
        height: '20px',
        background: 'transparent',
      }}
    />
  );
};

export default IntersectionObserverComponent;
3. Error Handling and Retry
<InfiniteDataFetcher
  queryKey={['comments']}
  queryFn={({ pageParam }) => fetchComments(pageParam)}
  options={{
    retry: 3,  // Retry failed requests up to 3 times
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
  }}
>
  {({ 
    data, 
    error, 
    fetchNextPage, 
    hasNextPage,
    isError 
  }) => (
    <>
      {isError && (
        <ErrorBanner 
          message={error?.message || 'Failed to load comments'}
          onRetry={fetchNextPage}
        />
      )}
      {data?.map(comment => (
        <CommentCard key={comment.id} comment={comment} />
      ))}
    </>
  )}
</InfiniteDataFetcher>

DataFetcher Props

PropTypeDescriptionDefaultRequired
queryKeyQueryKeyUnique key for the queryundefinedtrue
queryFn(context: { pageParam }) => Promise<TItem>Function to fetch paginated dataundefinedfalse
urlstringApi url for fetching dataundefinedfalse
queryParamsRecord<string, any>query parameters to concatenate with the urlundefinedfalse
optionsUseInfiniteQueryOptionsReact Query infinite query optionsundefinedfalse
childrenReactNodeRender function with query resultsundefinedtrue

InfiniteDataFetcher Props

PropTypeDescriptionDefaultRequired
queryKeyQueryKeyUnique key for the queryundefinedtrue
queryFn(context: { pageParam }) => Promise<TItem>Function to fetch paginated dataundefinedfalse
urlstringApi url for fetching dataundefinedfalse
queryParams(pageParam) => Record<string, any>query parameters to concatenate with the urlundefinedfalse
optionsUseInfiniteQueryOptionsReact Query infinite query optionsundefinedfalse
childrenReactNodeRender function with query resultsundefinedtrue
enableManualFetchbooleanToggle between auto and manual fetchingfalsefalse
triggerComponentReactNodeCustom component for manual loadingundefinedfalse
loadingComponentReactNodeComponent shown during loadingundefinedfalse
noMoreDataComponentReactNodeComponent shown when there is no more dataundefinedfalse

I'll come up with a demo real soon just extra busy with work and the one i tested it on looks like shit.

Contributing

  1. Fork the repository
  2. Create your feature branch
  3. If you can use Biomejs
  4. Commit your changes
  5. Push to the branch
  6. Create a new Pull Request

License

MIT License

0.9.5

8 months ago

0.9.4

8 months ago

0.9.3

8 months ago

0.9.2

9 months ago

0.9.1

9 months ago

0.9.0

9 months ago