Skip to content

useResourceCompose

Saurav-TB-Pandey edited this page Sep 18, 2026 · 1 revision

useResourceCompose

The useResourceCompose hook allows you to combine, merge, and derive reactive state from multiple independent useResource instances. It automatically subscribes to all dependent resources and re-computes the derived state whenever any dependency emits new data, with custom equality comparisons to prevent unnecessary component re-renders.

Alternatively accessible via the static alias useResource.compose().

Usage

1. Minimum Configuration (Basic)

The simplest way to use useResourceCompose is to provide a unique cache key, a map of dependent deps, and a selector function.

import { useResource, useResourceCompose } from 'react-hook-lab';

function UserStats({ userId }) {
  const user = useResource({
    key: `user:${userId}`,
    fetcher: () => fetch(`/api/users/${userId}`).then(res => res.json()),
  });

  const posts = useResource({
    key: `posts:${userId}`,
    fetcher: () => fetch(`/api/users/${userId}/posts`).then(res => res.json()),
  });

  // Compose user and posts into a single summary resource
  const summary = useResourceCompose({
    key: `summary:${userId}`,
    deps: { user, posts },
    selector: ({ user, posts }) => ({
      name: user?.name ?? "Guest",
      postCount: posts?.length ?? 0,
    }),
  });

  if (summary.loading && !summary.data) return <div>Loading summary...</div>;
  if (summary.error) return <div>Error loading summary</div>;

  return (
    <div>
      <h3>{summary.data?.name}</h3>
      <p>Total Posts: {summary.data?.postCount}</p>
    </div>
  );
}

2. Common Usage (With Status Aggregation & Selectors)

In real-world applications, you often need to derive data from disparate endpoints (e.g. auth user and shopping cart) and handle aggregated loading and error states cleanly.

import { useResource, useResourceCompose } from 'react-hook-lab';

function HeaderCart() {
  const auth = useResource({
    key: 'auth:current-user',
    fetcher: () => fetch('/api/me').then(r => r.json()),
  });

  const cart = useResource({
    key: 'cart:items',
    fetcher: () => fetch('/api/cart').then(r => r.json()),
  });

  const cartSummary = useResourceCompose({
    key: 'cart:summary',
    deps: { auth, cart },
    selector: ({ auth, cart }) => {
      const items = cart?.items ?? [];
      const totalAmount = items.reduce((sum, i) => sum + i.price * i.qty, 0);
      return {
        userName: auth?.username,
        itemCount: items.length,
        totalAmount,
        hasDiscount: (auth?.tier === 'vip') && totalAmount > 100,
      };
    },
  });

  return (
    <header>
      <span>Hello, {cartSummary.data?.userName || "Shopper"}</span>
      <span>Cart ({cartSummary.data?.itemCount || 0} items)</span>
      <span>Total: ${cartSummary.data?.totalAmount || 0}</span>
      {cartSummary.data?.hasDiscount && <span>VIP Discount Applied!</span>}
    </header>
  );
}

3. Advanced Usage (Custom Equality Function & Static API)

To optimize performance for high-frequency updates, useResourceCompose accepts an equalityFn (defaults to deepEqual) to skip state updates when the selected value remains structurally identical.

import { useResource, deepEqual } from 'react-hook-lab';

function HighPerformanceView() {
  const resourceA = useResource({ key: 'source-a', fetcher: fetchA });
  const resourceB = useResource({ key: 'source-b', fetcher: fetchB });

  // Can be called directly via useResource.compose()
  const composed = useResource.compose({
    key: 'composed:performance',
    deps: { resourceA, resourceB },
    selector: ({ resourceA, resourceB }) => {
      return {
        aId: resourceA?.id,
        bCount: resourceB?.count,
      };
    },
    // Custom fine-grained equality check to avoid re-renders
    equalityFn: (prev, next) => {
      return prev?.aId === next?.aId && prev?.bCount === next?.bCount;
    },
  });

  return (
    <div>
      <p>ID: {composed.data?.aId}</p>
      <p>Count: {composed.data?.bCount}</p>
    </div>
  );
}

API

Parameters

useResourceCompose<TDeps, R>(config) takes an object:

  • key: string (required): The unique key identifying the derived resource.
  • deps: TDeps (required): A record/object mapping arbitrary names to Resource<any> instances.
  • selector: (values: { [K in keyof TDeps]: TDeps[K]["data"] }) => R (required): Pure transformer function that receives the resolved .data from each dependency and returns the derived output.
  • equalityFn?: (a: R, b: R) => boolean (optional): Comparison function used to determine if the derived output actually changed. Defaults to deepEqual.

Returns (Resource<R>)

Returns a standard Resource<R> instance representing the derived state:

  • data: R | undefined: The latest derived output from selector.
  • loading: boolean: True if the composed resource is initializing or any dependency is loading without data.
  • error: unknown | undefined: Captured errors if any dependency or selector fails.
  • status: "idle" | "loading" | "error" | "success" | "refreshing": Current status.
  • mutate(updater): Manually override or update the composed state.
  • refresh(): Force a re-computation of the derived state.

Clone this wiki locally