All files / src/components/SearchPanel/hooks useOptimisticResults.ts

98.55% Statements 68/69
88.23% Branches 15/17
100% Functions 7/7
98.55% Lines 68/69

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 1451x   1x                                     1x 1x 9x 9x 9x 3x 3x 3x     9x           9x 9x 1x 1x   9x                   9x 9x 1x 1x   9x 9x 9x 9x 9x 9x   1x                                     1x 1x 13x 13x 13x 5x 5x   5x 5x 3x 3x   5x 1x 1x     5x 1x   5x   5x 5x     13x       13x 13x 3x 3x   13x       13x 13x 1x 1x   13x       13x 13x 1x 1x   13x 13x 13x 13x 13x 13x 13x  
import { useOptimistic } from 'react';
 
/**
 * React v19 `useOptimistic` wrapper for streaming search results.
 * Provides optimistic updates so the UI reflects new products the
 * instant they arrive, before the full search has completed.
 * React automatically rolls back the optimistic state if a
 * transition that produced it is interrupted or errors.
 * @param confirmedResults - The server-confirmed product list (source of truth)
 * @returns An object with the current `results` array, an `addResult`
 *   function for single products, and `addResultsBatch` for bulk inserts.
 * @example
 * ```tsx
 * const { results, addResult } = useOptimisticResults(searchResults);
 *
 * for await (const product of stream) {
 *   addResult(product);   // UI updates immediately
 * }
 * // results => [{ ...product, _id: 0 }, { ...product, _id: 1 }, ...]
 * ```
 * @source
 */
export function useOptimisticResults(confirmedResults: Product[]) {
  const [optimisticResults, addOptimisticResult] = useOptimistic(
    confirmedResults,
    (state: Product[], newProduct: Product) => {
      // `_id` is the positional row index; the product's real `id` is preserved.
      return [...state, { ...newProduct, _id: state.length }];
    },
  );
 
  /**
   * Optimistically append a single product to the results list.
   * The entry appears in the UI instantly and is later reconciled
   * when `confirmedResults` updates.
   * @param product - The product to add
   * @source
   */
  const addResult = (product: Product) => {
    addOptimisticResult(product);
  };
 
  /**
   * Optimistically append multiple products at once.
   * Useful when several results arrive in the same tick.
   * @param products - Array of products to add
   * @example
   * ```ts
   * addResultsBatch([productA, productB]);
   * // results => [...existing, productA, productB]
   * ```
   * @source
   */
  const addResultsBatch = (products: Product[]) => {
    products.forEach((product) => addOptimisticResult(product));
  };
 
  return {
    results: optimisticResults,
    addResult,
    addResultsBatch,
  };
}
 
/**
 * Extended optimistic-results hook that tracks a per-item pending state.
 * Each product passes through three lifecycle stages:
 * `add` (pending) → `confirm` (persisted) or `error` (removed).
 * This lets the UI render a loading indicator on rows that are not
 * yet confirmed.
 * @param confirmedResults - The server-confirmed product list (source of truth)
 * @returns An object with `results`, `addPendingResult`, `confirmResult`,
 *   and `removeFailedResult` functions.
 * @example
 * ```tsx
 * const { results, addPendingResult, confirmResult, removeFailedResult } =
 *   useOptimisticResultsWithPending(searchResults);
 *
 * addPendingResult(product);   // row appears with isPending: true
 * confirmResult(product);      // isPending flips to false
 * removeFailedResult(product); // row is removed from the list
 * ```
 * @source
 */
export function useOptimisticResultsWithPending(confirmedResults: Product[]) {
  const [optimisticResults, addOptimisticResult] = useOptimistic(
    confirmedResults,
    (
      state: Product[],
      action: { type: 'add' | 'confirm' | 'error'; product: Product; tempId?: string },
    ) => {
      switch (action.type) {
        case 'add':
          // `_id` positions the row; confirm/error match on that same positional key.
          return [...state, { ...action.product, _id: state.length, isPending: true }];
 
        case 'confirm':
          return state.map((item) =>
            item._id === action.product._id ? { ...action.product, isPending: false } : item,
          );
 
        case 'error':
          return state.filter((item) => item._id !== action.product._id);
 
        default:
          return state;
      }
    },
  );
 
  /**
   * Add a product in the pending state (`isPending: true`).
   * @param product - The product to insert optimistically
   * @source
   */
  const addPendingResult = (product: Product) => {
    addOptimisticResult({ type: 'add', product });
  };
 
  /**
   * Mark a previously pending product as confirmed (`isPending: false`).
   * @param product - The confirmed product (must have a matching `_id`)
   * @source
   */
  const confirmResult = (product: Product) => {
    addOptimisticResult({ type: 'confirm', product });
  };
 
  /**
   * Remove a product that failed processing from the optimistic list.
   * @param product - The failed product (must have a matching `_id`)
   * @source
   */
  const removeFailedResult = (product: Product) => {
    addOptimisticResult({ type: 'error', product });
  };
 
  return {
    results: optimisticResults,
    addPendingResult,
    confirmResult,
    removeFailedResult,
  };
}