Skip to content

InventoryProvider

Any module displaying product/inventory data. Place at page/module level.

Static catalogs without BFF inventory API. Adds ~80KB gzipped (React Query).

  • useInventory
  • useInventoryOptional
  • Watch isCpaReady / isPriceReady before assuming full product shape.
  • Use getProduct(styleColor) for async IDB-backed lookups.
  • productMapRef gives sync O(1) access when burst load has landed.
  • Context value shape (all query methods) is documented via useInventory in the Hooks reference.

Products live in IndexedDB (not React state) with an in-memory Map for sync O(1) reads — a store’s full catalog never sits in a component tree. Two independent flows share the same IndexedDB cache and in-memory Map: a background pipeline that progressively enriches the whole store catalog after mount, and an on-demand lookup (getProduct()) that resolves one style-color at a time, used whenever a component asks for a product that isn’t confirmed fresh yet.

Background loading (runs once per storeId)

Section titled “Background loading (runs once per storeId)”
flowchart TD
    A[Mount / storeId change] --> B["Style Management (SM)<br/>IDB 10-min TTL, else BFF"]
    B -->|populate Map, render immediately| C["loadCPADataInBursts()<br/>guarded — one run per storeId"]
    C --> D["Fetch uncached CPA<br/>50 style-codes/request, sequential rounds"]
    D -->|save to IDB| E["Enrich #1: refetchStyleManagement()<br/>re-runs the SM query — merges SM + CPA<br/>isCpaReady = true"]
    D -.concurrently with Enrich #1.-> F["Fetch prices<br/>from cached CPA globalProductIds"]
    E --> G["Enrich #2: refetchStyleManagement()<br/>re-runs the SM query again — prices merged in<br/>isPriceReady = true"]
    F -.save to IDB, then re-enter the SM query.-> G
    G --> H["Schedule refresh timer (2.5 hr)<br/>re-enters this pipeline to keep prices fresh"]

refetchStyleManagement() is the same refetch returned by the single useQuery(['inventory', storeId]) that loaded SM in the first place — it isn’t a second network call by itself. Re-running its queryFn re-reads SM (now already cached from the initial load), reads whatever is currently in the cpaData/priceData IDB stores, merges them with mergeProductWithCPA, writes the merged result back to the inventory store, and repopulates productMapRef. Enrich #1 and Enrich #2 are the same function call made twice — once after CPA lands, once after prices land — so each pass only picks up whichever reference table is newly populated.

getProduct(styleColor) — on-demand resolution

Section titled “getProduct(styleColor) — on-demand resolution”
flowchart TD
    A["getProduct(styleColor)"] --> B["in-memory Map<br/>O(1) sync read"]
    B -->|miss| C["Style Management<br/>reads IDB"]
    C -->|miss| D["additionalEnriched<br/>on-demand cache"]
    D --> E{Has a description AND a valid image?}
    E -->|Yes| F["Return as-is<br/>cached in Map"]
    E -->|No| G["Fetch item-inquiry<br/>merges CPA + SOH + inbound + price"]
    G -->|isFetchingCpa = true while in flight| H{Result}
    H -->|Success| I["Save to additionalEnriched<br/>replaces the record wholesale, cached in Map"]
    H -->|Failure| J["Return existing partial record<br/>or undefined if nothing was found"]

Description and image both come from the same item-inquiry response, so there is one gate, not several: if either is missing, the whole record is replaced by a single fetch rather than patched field-by-field. isFetchingCpa tracks these item-inquiry calls specifically — it’s a counter under the hood, so it stays true while any concurrent getProduct() calls (e.g. Promise.all over a page of rows) are in flight, and only flips back to false once the last one settles.

Store TTL Role
inventory 10 min Primary table — SM products merged with CPA + Price
cpaData 7 days Reference table — product details (images, sizes, descriptions)
priceData 3 hours Reference table — current selling prices, auto-refreshed at 2.5hr
additionalEnriched 24 hours On-demand products resolved via getProduct()’s item-inquiry fallback

DO

  • Place one provider per store, at the module/page level.
  • Prefer productMapRef.current.get(styleColor) for sync reads once productMapVersion confirms data has landed.
  • Use getProduct() for anything that might be missing from Style Management — it handles the fallback chain.
  • Show isFetchingCpa when calling getProduct() for rows that may trigger a live fetch.

DON’T

  • Don’t copy the full product catalog into React state — read from the Map/IDB on demand.
  • Don’t call query functions during render — use useEffect.
  • Don’t nest multiple InventoryProviders for the same store.

Props for InventoryProvider. The provider mounts immediately and begins loading Style Management data. Set loggedIn={false} on login pages or during session expiry to pause all BFF fetches and prevent 401/403 errors downstream.

Prop Type Required Description
accessToken null | string Yes Current access token string. Pass null when unauthenticated.
bffUrl string No Explicit BFF base URL override (e.g. http://localhost:4000). When provided it takes precedence over isProd URL resolution. Use for local dev or custom deploy targets.
children React.ReactNode Yes Child components that consume inventory data.
isProd boolean No Whether the app is running in production mode. Controls which BFF and Nike API base URLs are used. Default: false
loggedIn boolean No Auth gate — when false the provider mounts but skips all BFF fetches and resets transient in-flight state. Set to false on login pages or when the session has expired to prevent 401/403 errors. Set to true once a valid token is available. Default: false
storeId string Yes Store UUID to fetch inventory for.

accessToken is a string (not a callback). Set loggedIn=false on login/expired-session pages to pause fetches.

import { InventoryProvider } from '@nike/whisker-component-library';
<InventoryProvider
storeId={storeId}
accessToken={accessToken}
loggedIn={isLoggedIn}
isProd={isProd}
>
<ProductGrid />
</InventoryProvider>;
function ProductCard({ styleColor }) {
const { isCpaReady, isPriceReady, getProduct } = useInventory();
const [product, setProduct] = useState(null);
// Re-read once each enrichment phase lands (images/descriptions, then prices)
useEffect(() => {
getProduct(styleColor).then(setProduct);
}, [isCpaReady, isPriceReady, styleColor]);
if (!product) return <Skeleton />;
return <Card image={product.imagesUrl} price={product.currentPrice} />;
}