Lightweight data fetching and caching utilities for Smart TV applications
npm install @smart-tv/queryA 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.
- π 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
Install the package using your preferred package manager:
``bashnpm
npm install @smart-tv/query
Quick Start
$3
Wrap your app with
QueryClientProvider and create a QueryClient instance:`tsx
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 (
);
}
`$3
Use
useQuery to fetch and cache data:`tsx
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
Loading...;
if (status === "error") return Error: {error.message}; return (
{data.map((movie) => (
{movie.title}
))}
);
}
`Core Concepts
$3
The
QueryClient is the core of the library. It manages cache, handles deduplication, and coordinates all queries.`tsx
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
$3
Fetch and cache data with automatic cache management.
`tsx
const { data, error, status, refetch } = useQuery(queryKey, queryFn, options);
`Parameters:
-
queryKey: string | readonly unknown[] - Unique identifier for the query
- queryFn: () => Promise - Function that returns a promise with data
- options: QueryOptions - 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 - Function to manually refetch dataExample with dynamic parameters:
`tsx
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 ;
return
{data.title};
}
`Example with data transformation:
`tsx
const { data } = useQuery(
["movies"],
() => fetch("/api/movies").then((res) => res.json()),
{
select: (data) => data.filter((movie) => movie.rating > 4),
}
);
`$3
Execute mutations (POST, PUT, DELETE) with success/error callbacks.
`tsx
const { mutate, data, error, status } = useMutation(mutationFn, options);
`Parameters:
-
mutationFn: (variables: TVariables) => Promise - Function that performs the mutation
- options: MutationOptions - Optional callbacksReturns:
-
mutate: (variables: TVariables) => Promise - 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 statusExample:
`tsx
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 (
);
}
`$3
Fetch paginated data with infinite scroll support.
`tsx
const { data, isFetching, fetchNext, hasNextPage } = useInfiniteQuery(
queryKey,
fetchPageFn,
options
);
`Parameters:
-
queryKey: QueryKey - Unique identifier for the infinite query
- fetchPageFn: (cursor?: string | number | null) => Promise - Function to fetch each page
- options: InfiniteQueryOptions - Configuration optionsReturns:
-
data: TItem[] - Flattened array of all items from all pages
- isFetching: boolean - Whether currently fetching a page
- fetchNext: () => Promise - Function to fetch the next page
- hasNextPage: boolean - Whether there are more pages to fetchExample:
`tsx
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 (
{data.map((movie) => (
))} {hasNextPage && (
)}
);
}
`$3
#### invalidateQueries
Invalidate queries to mark them as stale and optionally refetch.
`tsx
// 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.
`tsx
const cachedMovies = queryClient.getQueryData(["movies"]);
`#### setQueryData
Manually update cached data.
`tsx
// 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).
`tsx
const movies = await queryClient.fetchQuery(
["movies"],
() => fetch("/api/movies").then((res) => res.json()),
{ staleTime: 1000 * 60 }
);
`Advanced Features
$3
The library includes a powerful XHR-based fetcher with progress tracking and timeout support:
`tsx
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$3
Automatically refetch stale data when the TV app regains focus:
`tsx
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
});
`$3
Update UI optimistically before mutation completes:
`tsx
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);
};
`$3
Execute queries that depend on other queries:
`tsx
// 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
}
);
`$3
Execute multiple queries in parallel:
`tsx
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 ;
}
return (
);
}
`Complete Example
Here's a comprehensive example showing various features:
`tsx
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 ;
if (status === "error") return ;
return (
{data.map((movie) => (
))}
);
}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 (
{data?.title}
);
}// App
function App() {
return (
);
}
`TypeScript Support
Full TypeScript support with excellent type inference:
`tsx
import { useQuery, QueryClient, QueryOptions } from "@smart-tv/query";interface Movie {
id: number;
title: string;
rating: number;
}
// Type-safe query
const { data } = useQuery(["movies"], () =>
fetch("/api/movies").then((res) => res.json())
);
// data is typed as Movie[] | undefined
// Type-safe mutation
const { mutate } = useMutation((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 supportBrowser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Smart TV browsers (Tizen 4.0+, webOS 4.0+)
Development
$3
`bash
Install dependencies
pnpm installBuild
pnpm --filter=@smart-tv/query buildDevelopment mode
pnpm --filter=@smart-tv/query dev
``Contributions are welcome! Please follow the monorepo conventions and add tests for new features.
See CONTRIBUTING.md for more details.
- @smart-tv/ui - React component library for Smart TV apps
- @smart-tv/player - Video player with focus support
- create-smart-tv - CLI to scaffold Smart TV projects
BSD 3-Clause License - see LICENSE for details.
- π Documentation
- π Report Issues
- π¬ Discussions
---
Made with β€οΈ for Smart TV developers