Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

@smart-tv/query

A lightweight, dependency-free data fetching and caching library optimized for Smart TV applications. Built with performance and simplicity in mind, it provides powerful features without the bloat of larger libraries.

Features

  • 🚀 Lightweight - Zero dependencies, tree-shakeable, optimized for Smart TV performance
  • 💾 Smart Caching - Configurable staleTime and cacheTime with automatic cache management
  • 🔄 Request Deduplication - Automatic deduplication of concurrent identical requests
  • React Hooks - useQuery, useMutation, useInfiniteQuery for seamless integration
  • 🔌 XHR & Fetch Support - Built-in XHR fetcher with progress tracking and abort support
  • ♾️ Infinite Queries - Built-in support for paginated and infinite scroll data
  • 🎯 Window Focus Refetch - Automatically refetch stale data when window regains focus
  • 🔧 TypeScript First - Full TypeScript support with excellent type inference
  • 🪶 Small Bundle Size - Minimal footprint for faster load times on TV devices

Installation

Install the package using your preferred package manager:

# npm
npm install @smart-tv/query

# pnpm
pnpm add @smart-tv/query

# yarn
yarn add @smart-tv/query

Quick Start

Basic Setup

Wrap your app with QueryClientProvider and create a QueryClient instance:

import { QueryClient, QueryClientProvider } from "@smart-tv/query";

const queryClient = new QueryClient({
  staleTime: 1000 * 60 * 5, // 5 minutes
  cacheTime: 1000 * 60 * 10, // 10 minutes
  retry: 3, // Retry failed requests 3 times
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

Basic Query

Use useQuery to fetch and cache data:

import { useQuery } from "@smart-tv/query";

function Movies() {
  const { data, error, status, refetch } = useQuery(["movies"], () =>
    fetch("/api/movies").then((res) => res.json())
  );

  if (status === "loading") return <div>Loading...</div>;
  if (status === "error") return <div>Error: {error.message}</div>;

  return (
    <div>
      {data.map((movie) => (
        <div key={movie.id}>{movie.title}</div>
      ))}
    </div>
  );
}

Core Concepts

QueryClient

The QueryClient is the core of the library. It manages cache, handles deduplication, and coordinates all queries.

import { QueryClient } from "@smart-tv/query";

const queryClient = new QueryClient({
  staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes
  cacheTime: 1000 * 60 * 10, // Cache persists for 10 minutes after unused
  retry: 3, // Retry failed requests 3 times
  enabled: true, // Enable queries by default
  keepPreviousData: false, // Whether to keep previous data during refetch
});

Configuration Options:

Option Type Default Description
staleTime number 0 Time in ms before data is considered stale
cacheTime number 5min Time in ms before unused cache is garbage collected
retry number 0 Number of retry attempts for failed requests
enabled boolean true Enable/disable queries globally
keepPreviousData boolean false Keep previous data during refetch
refetchOnWindowFocus boolean false Refetch when window regains focus
refetchOnMount boolean true Refetch on component mount if stale

API Reference

useQuery

Fetch and cache data with automatic cache management.

const { data, error, status, refetch } = useQuery(queryKey, queryFn, options);

Parameters:

  • queryKey: string | readonly unknown[] - Unique identifier for the query
  • queryFn: () => Promise<T> - Function that returns a promise with data
  • options: QueryOptions<T> - Optional configuration (overrides client defaults)

Returns:

  • data: T | undefined - The fetched data
  • error: unknown | undefined - Error if the query failed
  • status: 'idle' | 'loading' | 'success' | 'error' - Current query status
  • refetch: () => Promise<T> - Function to manually refetch data

Example with dynamic parameters:

function MovieDetails({ movieId }) {
  const { data, status } = useQuery(
    ["movie", movieId],
    () => fetch(`/api/movies/${movieId}`).then((res) => res.json()),
    {
      staleTime: 1000 * 60 * 10, // 10 minutes
      enabled: !!movieId, // Only fetch if movieId exists
    }
  );

  if (status === "loading") return <Spinner />;
  return <div>{data.title}</div>;
}

Example with data transformation:

const { data } = useQuery(
  ["movies"],
  () => fetch("/api/movies").then((res) => res.json()),
  {
    select: (data) => data.filter((movie) => movie.rating > 4),
  }
);

useMutation

Execute mutations (POST, PUT, DELETE) with success/error callbacks.

const { mutate, data, error, status } = useMutation(mutationFn, options);

Parameters:

  • mutationFn: (variables: TVariables) => Promise<TData> - Function that performs the mutation
  • options: MutationOptions<TData, TVariables> - Optional callbacks

Returns:

  • mutate: (variables: TVariables) => Promise<TData> - Function to trigger the mutation
  • data: TData | undefined - Response data from the mutation
  • error: unknown | undefined - Error if mutation failed
  • status: 'idle' | 'loading' | 'success' | 'error' - Current mutation status

Example:

import { useMutation } from "@smart-tv/query";

function AddMovieForm() {
  const { mutate, status } = useMutation(
    (newMovie) =>
      fetch("/api/movies", {
        method: "POST",
        body: JSON.stringify(newMovie),
        headers: { "Content-Type": "application/json" },
      }).then((res) => res.json()),
    {
      onSuccess: (data) => {
        console.log("Movie added:", data);
        // Invalidate and refetch movies list
        queryClient.invalidateQueries(["movies"], { refetch: true });
      },
      onError: (error) => {
        console.error("Failed to add movie:", error);
      },
    }
  );

  const handleSubmit = (movie) => {
    mutate(movie);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
      <button type="submit" disabled={status === "loading"}>
        {status === "loading" ? "Adding..." : "Add Movie"}
      </button>
    </form>
  );
}

useInfiniteQuery

Fetch paginated data with infinite scroll support.

const { data, isFetching, fetchNext, hasNextPage } = useInfiniteQuery(
  queryKey,
  fetchPageFn,
  options
);

Parameters:

  • queryKey: QueryKey - Unique identifier for the infinite query
  • fetchPageFn: (cursor?: string | number | null) => Promise<TPage> - Function to fetch each page
  • options: InfiniteQueryOptions<TItem> - Configuration options

Returns:

  • data: TItem[] - Flattened array of all items from all pages
  • isFetching: boolean - Whether currently fetching a page
  • fetchNext: () => Promise<void> - Function to fetch the next page
  • hasNextPage: boolean - Whether there are more pages to fetch

Example:

import { useInfiniteQuery } from "@smart-tv/query";

function MovieList() {
  const { data, isFetching, fetchNext, hasNextPage } = useInfiniteQuery(
    ["movies", "infinite"],
    async (cursor) => {
      const res = await fetch(`/api/movies?cursor=${cursor || ""}`);
      return res.json();
    },
    {
      mapPage: (raw) => ({
        items: raw.results,
        nextCursor: raw.nextCursor,
      }),
      getHasNext: (pages) => {
        const lastPage = pages[pages.length - 1];
        return !!lastPage?.nextCursor;
      },
    }
  );

  return (
    <div>
      {data.map((movie) => (
        <MovieCard key={movie.id} movie={movie} />
      ))}

      {hasNextPage && (
        <button onClick={fetchNext} disabled={isFetching}>
          {isFetching ? "Loading..." : "Load More"}
        </button>
      )}
    </div>
  );
}

QueryClient Methods

invalidateQueries

Invalidate queries to mark them as stale and optionally refetch.

// Invalidate and refetch a specific query
queryClient.invalidateQueries(["movies"], { refetch: true });

// Invalidate without refetching
queryClient.invalidateQueries(["movies"]);

// Invalidate all queries
queryClient.invalidateQueries();

getQueryData

Get cached data for a specific query.

const cachedMovies = queryClient.getQueryData(["movies"]);

setQueryData

Manually update cached data.

// Direct update
queryClient.setQueryData(["movies"], newMoviesArray);

// Update with function
queryClient.setQueryData(["movies"], (oldData) => {
  return [...oldData, newMovie];
});

fetchQuery

Imperatively fetch a query (useful outside of React components).

const movies = await queryClient.fetchQuery(
  ["movies"],
  () => fetch("/api/movies").then((res) => res.json()),
  { staleTime: 1000 * 60 }
);

Advanced Features

Custom Fetcher with XHR

The library includes a powerful XHR-based fetcher with progress tracking and timeout support:

import { xhrFetcher, tvFetch } from "@smart-tv/query";

// Basic usage
const { data } = useQuery(["movie", id], () =>
  xhrFetcher(`/api/movies/${id}`, {
    method: "GET",
    headers: { Authorization: "Bearer token" },
  })
);

// With progress tracking
const { mutate } = useMutation((file) =>
  xhrFetcher("/upload", {
    method: "POST",
    body: file,
    timeout: 30000, // 30 seconds
    onUploadProgress: (sent, total) => {
      console.log(`Uploaded ${sent} / ${total}`);
    },
  })
);

// With abort signal
const controller = new AbortController();
xhrFetcher("/api/data", { signal: controller.signal });
// Later: controller.abort()

XHR Options:

  • method: HTTP method (GET, POST, PUT, DELETE, etc.)
  • headers: Request headers
  • body: Request body (auto-stringified for objects)
  • responseType: Response type ('json', 'text', 'blob', 'arraybuffer')
  • timeout: Request timeout in milliseconds
  • withCredentials: Include cookies in cross-origin requests
  • onUploadProgress: Upload progress callback
  • onDownloadProgress: Download progress callback
  • signal: AbortSignal for request cancellation

Window Focus Refetching

Automatically refetch stale data when the TV app regains focus:

const queryClient = new QueryClient({
  refetchOnWindowFocus: true,
});

// Or per-query
const { data } = useQuery(["movies"], fetchMovies, {
  refetchOnWindowFocus: true,
  staleTime: 1000 * 60 * 5, // Only refetch if older than 5 minutes
});

Optimistic Updates

Update UI optimistically before mutation completes:

const { mutate } = useMutation(
  (updatedMovie) =>
    fetch(`/api/movies/${updatedMovie.id}`, {
      method: "PUT",
      body: JSON.stringify(updatedMovie),
    }),
  {
    onSuccess: (newData) => {
      // Update cache with server response
      queryClient.setQueryData(["movie", newData.id], newData);
      queryClient.invalidateQueries(["movies"]);
    },
  }
);

// Optimistic update before mutation
const handleUpdate = (movie) => {
  // Update cache immediately
  queryClient.setQueryData(["movie", movie.id], movie);

  // Then trigger mutation
  mutate(movie);
};

Dependent Queries

Execute queries that depend on other queries:

// First query
const { data: user } = useQuery(["user"], fetchUser);

// Second query depends on first
const { data: posts } = useQuery(
  ["posts", user?.id],
  () => fetchUserPosts(user.id),
  {
    enabled: !!user?.id, // Only run when user.id exists
  }
);

Parallel Queries

Execute multiple queries in parallel:

function Dashboard() {
  const movies = useQuery(["movies"], fetchMovies);
  const shows = useQuery(["shows"], fetchShows);
  const trending = useQuery(["trending"], fetchTrending);

  if (movies.status === "loading" || shows.status === "loading") {
    return <Spinner />;
  }

  return (
    <div>
      <MovieSection data={movies.data} />
      <ShowSection data={shows.data} />
      <TrendingSection data={trending.data} />
    </div>
  );
}

Complete Example

Here's a comprehensive example showing various features:

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
  useMutation,
} from "@smart-tv/query";

// Create client
const queryClient = new QueryClient({
  staleTime: 1000 * 60 * 5,
  cacheTime: 1000 * 60 * 10,
  retry: 3,
  refetchOnWindowFocus: true,
});

// API functions
const fetchMovies = () => fetch("/api/movies").then((res) => res.json());
const fetchMovie = (id) => fetch(`/api/movies/${id}`).then((res) => res.json());
const addToWatchlist = (movieId) =>
  fetch("/api/watchlist", {
    method: "POST",
    body: JSON.stringify({ movieId }),
    headers: { "Content-Type": "application/json" },
  }).then((res) => res.json());

// Components
function MovieList() {
  const { data, status, refetch } = useQuery(["movies"], fetchMovies, {
    select: (data) => data.filter((m) => m.rating > 3),
  });

  if (status === "loading") return <Spinner />;
  if (status === "error") return <Error />;

  return (
    <div>
      <button onClick={refetch}>Refresh</button>
      {data.map((movie) => (
        <MovieCard key={movie.id} movie={movie} />
      ))}
    </div>
  );
}

function MovieDetails({ movieId }) {
  const { data } = useQuery(["movie", movieId], () => fetchMovie(movieId), {
    enabled: !!movieId,
  });

  const { mutate, status } = useMutation(addToWatchlist, {
    onSuccess: () => {
      queryClient.invalidateQueries(["watchlist"]);
      alert("Added to watchlist!");
    },
  });

  return (
    <div>
      <h1>{data?.title}</h1>
      <button onClick={() => mutate(movieId)} disabled={status === "loading"}>
        Add to Watchlist
      </button>
    </div>
  );
}

// App
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MovieList />
    </QueryClientProvider>
  );
}

TypeScript Support

Full TypeScript support with excellent type inference:

import { useQuery, QueryClient, QueryOptions } from "@smart-tv/query";

interface Movie {
  id: number;
  title: string;
  rating: number;
}

// Type-safe query
const { data } = useQuery<Movie[]>(["movies"], () =>
  fetch("/api/movies").then((res) => res.json())
);
// data is typed as Movie[] | undefined

// Type-safe mutation
const { mutate } = useMutation<Movie, { title: string }>((newMovie) =>
  fetch("/api/movies", {
    method: "POST",
    body: JSON.stringify(newMovie),
  }).then((res) => res.json())
);
// mutate expects { title: string }

Documentation

For comprehensive documentation, interactive examples, and best practices, visit:

📚 https://smart-tv-docs.vercel.app/components/query

Performance Tips for Smart TV

  1. Use appropriate staleTime: Set longer staleTime (5-10 minutes) for data that doesn't change frequently
  2. Implement pagination: Use useInfiniteQuery for large datasets
  3. Enable window focus refetch: Ensure users see fresh data when returning to the app
  4. Optimize cache size: Set reasonable cacheTime to prevent memory issues on low-end devices
  5. Use XHR fetcher: Better control over requests with timeout and abort support

Browser Support

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+
  • Smart TV browsers (Tizen 4.0+, webOS 4.0+)

Development

Building the package

# Install dependencies
pnpm install

# Build
pnpm --filter=@smart-tv/query build

# Development mode
pnpm --filter=@smart-tv/query dev

Contributing

Contributions are welcome! Please follow the monorepo conventions and add tests for new features.

See CONTRIBUTING.md for more details.

Related Packages

License

BSD 3-Clause License - see LICENSE for details.

Support


Made with ❤️ for Smart TV developers