InventoryProvider
When to use
Section titled “When to use”Any module displaying product/inventory data. Place at page/module level.
When NOT to use
Section titled “When NOT to use”Static catalogs without BFF inventory API. Adds ~80KB gzipped (React Query).
Related stores
Section titled “Related stores”useInventoryuseInventoryOptional
Common pitfalls
Section titled “Common pitfalls”- Watch
isCpaReady/isPriceReadybefore assuming full product shape. - Use
getProduct(styleColor)for async IDB-backed lookups. productMapRefgives sync O(1) access when burst load has landed.- Context value shape (all query methods) is documented via
useInventoryin the Hooks reference.
How it works
Section titled “How it works”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.
Cache layers (IndexedDB)
Section titled “Cache layers (IndexedDB)”| 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 |
Best practices
Section titled “Best practices”DO
- Place one provider per store, at the module/page level.
- Prefer
productMapRef.current.get(styleColor)for sync reads onceproductMapVersionconfirms data has landed. - Use
getProduct()for anything that might be missing from Style Management — it handles the fallback chain. - Show
isFetchingCpawhen callinggetProduct()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. |
Examples
Section titled “Examples”Module-level provider
Section titled “Module-level provider”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>;Looking up a single product
Section titled “Looking up a single product”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} />;}