npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dmrk/use-resource

v0.1.3

Published

A lightweight React library for efficient data fetching and caching with built-in Suspense support, preventing unnecessary re-renders and network requests.

Readme

use-resource

A lightweight React library for efficient data fetching and caching with built-in Suspense support, preventing unnecessary re-renders and network requests.

Table of Contents

Features

  • 🎯 Smart Caching - Automatic request deduplication and intelligent cache management
  • ⚡ React Suspense - First-class Suspense support for elegant loading states
  • 🔄 Automatic Revalidation - Configurable TTL-based cache invalidation
  • 🎨 Flexible Loading States - isLoading and isValidating for fine-grained UI control
  • 🔒 Type-safe - Full TypeScript support with automatic type inference
  • 🪶 Lightweight - Minimal dependencies, small bundle size
  • 🚀 Performance Optimized - Components sharing the same key share data and state

Live Demo

Explore an interactive demo showcasing some of the features:

  • Suspense Mode - React Suspense integration with loading fallbacks
  • Error Handling - ErrorBoundary catching errors from failed fetches
  • Optimistic Updates - Instant UI updates with mutate() and proper rollback
  • Shared State - Multiple components sharing the same resource key
  • Loading States - Fine-grained control with isLoading and isValidating

Run the demo locally:

cd example
pnpm install
pnpm dev

Installation

pnpm add @dmrk/use-resource

Quick Start

With Suspense (Recommended)

import { useResource } from '@dmrk/use-resource';

function User({ userId }: { userId: string }) {
  const { data } = useResource(`user-${userId}`, () =>
    fetch(`/api/users/${userId}`).then(res => res.json())
  );

  return <div>Hello, {data.name}!</div>;
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <User userId="123" />
    </Suspense>
  );
}

Without Suspense

function User({ userId }: { userId: string }) {
  const { data, error, isLoading, refetch } = useResource(
    `user-${userId}`,
    () => fetch(`/api/users/${userId}`).then(res => res.json()),
    { suspense: false }
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <p>Hello, {data.name}!</p>
      <button onClick={refetch}>Refresh</button>
    </div>
  );
}

API Reference

useResource(key, fetcher, options?)

The main hook for fetching and caching data.

Parameters:

  • key (string): Unique identifier for the resource. Components with the same key share cached data.
  • fetcher (() => T | Promise): Function that returns data synchronously or asynchronously.
  • options (optional):
    • suspense (boolean): Enable Suspense mode. Default: true
    • ttl (number): Time-to-live in seconds. Resource revalidates after expiration. Default: 60

Returns:

{
  data: T | undefined;           // The fetched data
  error: Error | undefined;      // Error if fetch failed
  isLoading: boolean;            // true during initial load (no data yet)
  isValidating: boolean;         // true during any fetch (initial or refetch)
  refetch: () => void;           // Manually trigger a refetch
}

State Behavior:

| Status | isLoading | isValidating | data | Description | | -------------- | --------- | ------------ | ------------ | -------------------------- | | pending | ✅ true | ✅ true | ❌ undefined | Initial fetch in progress | | fulfilled | ❌ false | ❌ false | ✅ available | Data loaded successfully | | revalidating | ❌ false | ✅ true | ✅ available | Refetching with stale data | | rejected | ❌ false | ❌ false | ❌ undefined | Fetch failed with error |

Examples:

// Basic usage
const { data } = useResource('todos', fetchTodos);

// With TTL (revalidates every 5 minutes)
const { data } = useResource('user', fetchUser, { ttl: 300 });

// Without Suspense
const { data, isLoading, error } = useResource('posts', fetchPosts, {
  suspense: false,
});

// Synchronous fetcher
const { data } = useResource('config', () => ({
  theme: 'dark',
  language: 'en',
}));

mutate(key, data) ⚠️ Experimental

Warning: This API is experimental and may change in future versions.

Manually update cached data without refetching. Useful for optimistic updates.

Parameters:

  • key (string): The resource key to update
  • data (T): New data to set in cache

Returns:

  • boolean: true if mutation succeeded, false if resource is currently pending/revalidating

Example:

import { mutate } from '@dmrk/use-resource';

function TodoList() {
  const { data: todos, refetch } = useResource('todos', fetchTodos, {
    suspense: false,
  });

  const addTodo = async (text: string) => {
    const newTodo = { id: Date.now(), text, completed: false };

    // Optimistically update UI
    mutate('todos', [...(todos || []), newTodo]);

    try {
      await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      });
    } catch (error) {
      // Revert on error by refetching
      refetch();
    }
  };

  return (
    <div>
      {todos?.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
      <button onClick={() => addTodo('New task')}>Add</button>
    </div>
  );
}

Limitations:

  • Cannot mutate resources that are currently pending or revalidating
  • Does not trigger network requests (use refetch() for that)
  • API may change in future versions

Advanced Usage

Shared State Between Components

Multiple components using the same key automatically share data and state:

function UserProfile() {
  const { data } = useResource('current-user', fetchCurrentUser);
  return <div>{data.name}</div>;
}

function UserAvatar() {
  const { data, refetch } = useResource('current-user', fetchCurrentUser);
  // ✅ Shares the same data as UserProfile
  // ✅ refetch() updates both components
  return <img src={data.avatar} onClick={refetch} />;
}

Conditional Fetching

function User({ userId }: { userId: string | null }) {
  const { data } = useResource(
    userId ? `user-${userId}` : 'no-user',
    userId ? () => fetchUser(userId) : () => null
  );

  if (!userId) return <div>No user selected</div>;
  return <div>{data.name}</div>;
}

Dependent Queries

function UserPosts({ userId }: { userId: string }) {
  const { data: user } = useResource(`user-${userId}`, () => fetchUser(userId));

  const { data: posts } = useResource(`posts-${user.id}`, () =>
    fetchUserPosts(user.id)
  );

  return <PostList posts={posts} />;
}

Error Boundaries with Suspense

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError) {
      return <div>Error: {this.state.error.message}</div>;
    }
    return this.props.children;
  }
}

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <User userId="123" />
      </Suspense>
    </ErrorBoundary>
  );
}

Best Practices

✅ Use Descriptive Keys

// Good: Descriptive, unique keys
useResource(`user-${userId}`, () => fetchUser(userId));
useResource(`posts-${userId}-page-${page}`, () => fetchPosts(userId, page));

// Bad: Generic keys
useResource('data', fetchData);

✅ Set Appropriate TTL

// Frequently changing data: short TTL
useResource('stock-price', fetchStockPrice, { ttl: 10 });

// Rarely changing data: long TTL
useResource('countries', fetchCountries, { ttl: 3600 });

// Static data: no TTL
useResource('app-config', fetchConfig);

✅ Handle Loading and Error States

// With Suspense: wrap in Suspense and ErrorBoundary
<ErrorBoundary>
  <Suspense fallback={<Spinner />}>
    <DataComponent />
  </Suspense>
</ErrorBoundary>;

// Without Suspense: check isLoading and error
const { data, isLoading, error } = useResource('data', fetchData, {
  suspense: false,
});
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;

⚠️ Avoid Dynamic Keys Without Memoization

// Bad: Creates new key on every render
useResource(`user-${Date.now()}`, fetchUser);

// Good: Stable key
const key = useMemo(() => `user-${userId}`, [userId]);
useResource(key, fetchUser);

TypeScript Support

Automatic type inference - TypeScript automatically infers the return type from your fetcher function:

interface User {
  id: string;
  name: string;
  email: string;
}

async function fetchUser(userId: string): Promise<User> {
  const res = await fetch(`/api/users/${userId}`);
  return res.json();
}

function UserProfile({ userId }: { userId: string }) {
  // ✅ TypeScript automatically infers data as User from fetchUser's return type
  // No need to pass generic <User>!
  const { data, error, refetch } = useResource(
    `user-${userId}`,
    () => fetchUser(userId),
    { suspense: false }
  );

  if (!data) return null;

  // ✅ TypeScript knows data.name, data.email exist
  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
}

// You can also use inline async functions with explicit return types
function PostList() {
  const { data } = useResource('posts', async (): Promise<Post[]> => {
    const res = await fetch('/api/posts');
    return res.json();
  });

  // ✅ data is automatically typed as Post[]
  return (
    <div>
      {data.map(post => (
        <PostItem key={post.id} post={post} />
      ))}
    </div>
  );
}

// Only pass generic when you can't infer the type (rare cases)
const { data } = useResource<User>(
  'user',
  () => fetch('/api/user').then(r => r.json()) // r.json() returns any
);

Comparison with Other Solutions

Note: use-resource is a minimal, experimental library built for exploration. It intentionally lacks many advanced features found in mature libraries. Choose based on your project's needs.

vs SWR

| Feature | SWR | use-resource | | ------------------------- | -------------------------------------------------------- | ------------------ | | Bundle Size | ~5KB | ~2KB | | Suspense Support | ✅ Optional | ✅ Default | | TypeScript Inference | Good | Excellent | | API Complexity | Moderate | Minimal | | Community & Ecosystem | Large, mature | Experimental | | Advanced Features | Middleware, focus revalidation, optimistic UI, mutations | Basic caching only | | Sync Fetchers | ❌ | ✅ | | Setup Required | None | None | | Production Ready | ✅ | ⚠️ Experimental |

vs React Query

| Feature | React Query | use-resource | | ------------------------- | ----------------------------------------------------------------------------------------------- | ------------------ | | Bundle Size | ~13KB | ~2KB | | Suspense Support | ✅ Via hooks | ✅ Default | | TypeScript Inference | Good | Excellent | | API Complexity | Complex (powerful) | Minimal (limited) | | Community & Ecosystem | Largest | Experimental | | Advanced Features | Devtools, infinite queries, mutations, prefetching, query invalidation, retry logic, pagination | Basic caching only | | Setup Required | QueryClient provider | None | | Learning Curve | Steeper | Minimal | | Production Ready | ✅ | ⚠️ Experimental |

vs Manual useEffect + useState

| Feature | useEffect + useState | use-resource | | ------------------------- | -------------------- | --------------------- | | Bundle Size | 0 (built-in) | ~2KB | | Code Required | High boilerplate | One hook call | | Caching | Manual | Automatic | | Request Deduplication | Manual | Automatic | | Race Conditions | Must handle manually | Handled automatically | | Loading States | Manual | Automatic | | Suspense Support | Manual integration | Built-in | | Type Safety | Manual typing | Inferred |

Requirements

  • React 18.0.0 or higher (for Suspense support)

License

ISC

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Author

dmrk