Matrix Ordering: Engineering High-Performance B2B Grids
A deep dive into building high-performance B2B matrix ordering grids for thousands of variants. Mastery of React Virtualization, State Management, and API Batching.
In the fast-paced world of B2C e-commerce, the user journey is linear: Browse, Select, Add to Cart. But B2B is a different beast entirely. A wholesale buyer for a fashion retailer doesn’t want to click “Add to Cart” fifty times for fifty different shirt sizes. They want efficiency. They want speed. They want a Matrix.
At Maison Code Paris, we have seen B2B portals crumble under their own weight. We have seen “enterprise” solutions that take 4 seconds to render a product page because they naïvely render 2,000 inputs for a single screw type. This is unacceptable. In the wholesale economy, friction doesn’t just annoy the user; it destroys the recurring revenue loop.
This guide is an engineering deep dive into building the “Excel of E-commerce”: The Matrix Ordering Grid.
Why Maison Code Discusses This
We build extensive B2B platforms for luxury houses and industrial giants. The difference between a tool that feels like a modern SaaS and one that feels like a legacy ERP is often the Matrix Grid. When a buyer can input quantities for 500 variants in seconds, using keyboard navigation, with zero lag, they feel professional. They feel respected.
We discuss this because it sits at the intersection of heavy technical constraints (DOM limits, API limits) and high-value user experience. Solving it requires more than just a UI library; it requires a fundamental understanding of how the browser renders and how state flows through an application.
The scalability Problem: O(n) rendering
Consider a simple B2B product: A T-shirt.
- Colors: 10
- Sizes: 8
- Total Variants: 80 inputs.
React allows you to render 80 inputs without breaking a sweat.
Now consider an industrial bolt.
- Lengths: 50
- Thread Pitches: 20
- Materials: 10
- Total Variants: 10,000 inputs.
If you attempt to render 10,000 <input> elements into the DOM simultaneously, your application will freeze. The bottleneck is not JavaScript execution; it is the Layout and Paint phases of the browser engine. Every time the user types a character into one box, if your state management is naive, React might attempt to reconcile the entire tree.
The Cost of Re-rendering
If you use a single useState object for the entire form:
const [formState, setFormState] = useState({});
Every keystroke triggers a re-render of the parent component, which then re-renders 10,000 children. Even with React.memo, the prop diffing alone for 10,000 components will cause noticeable input lag. In a professional tool, input lag is fatal.
Phase 1: Virtualization (The DOM Solution)
The first step to performance is acknowledging that the user cannot look at 10,000 inputs at once. A typical monitor displays perhaps 50 rows of data.
Virtualization (or “Windowing”) is the technique of rendering only the DOM nodes that are currently visible in the viewport. As the user scrolls, we destroy the nodes leaving the top of the screen and create new ones entering the bottom. The browser thinks it’s scrolling a 5,000px element, but the DOM only contains 50 divs.
We recommend TanStack Virtual (headless) or react-window for this implementation.
Implementation Pattern
Here is how we structure a virtualized grid for a B2B Matrix. We treat the grid as a coordinate system (Row, Column).
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
// The "Single Source of Truth" for the matrix data
// Ideally typically flattened or structured as Map<VariantID, Quantity>
type MatrixData = Record<string, number>;
export const VirtualizedMatrix = ({ rows, columns, data }: MatrixProps) => {
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // 50px row height
overscan: 5, // Render 5 extra rows for smooth scrolling
});
return (
<div
ref={parentRef}
style={{
height: `600px`,
overflow: 'auto',
}}
>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const rowData = rows[virtualRow.index];
return (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
display: 'flex',
}}
>
{/* Render Columns (Cells) here */}
<RowLabel label={rowData.name} />
{columns.map((col) => (
<MatrixInput
key={col.id}
variantId={getVariantId(rowData, col)}
/>
))}
</div>
);
})}
</div>
</div>
);
};
Key Takeaway: Virtualization reduces the Initial Load Time from 3.5s to 0.2s for large datasets. It is non-negotiable for catalogues exceeding 500 variants.
Phase 2: State Management (The Memory Solution)
Virtualization solves the rendering problem, but we still have a state problem. If we hold the state of 10,000 inputs in React State, updating one requires careful optimization to avoid triggering a tree-wide update.
The Signal Approach
At Maison Code, we prefer Signals (via @preact/signals-react or purely granular subscribers) or Uncontrolled Components for matrix grids.
If we use Uncontrolled Components, we bypass React’s render cycle entirely for the typing action.
- Read:
defaultValue={data.get(id)} - Write:
onChange={(e) => data.set(id, e.target.value)}(Direct Mutation or Ref update) - Submit: Read from the Ref/Map.
However, we often need Computed State (e.g., “Total Quantity: 150”). This puts us back in the Domain of reactivity.
The best modern approach is Zustand with Transient Updates.
// store.ts
import { create } from 'zustand';
interface MatrixStore {
quantities: Record<string, number>;
setQuantity: (id: string, qty: number) => void;
// Computed values derived in components or selectors within components
}
export const useMatrixStore = create<MatrixStore>((set) => ({
quantities: {},
setQuantity: (id, qty) => set((state) => ({
quantities: { ...state.quantities, [id]: qty }
})),
}));
// MatrixInput.tsx
// This component subscribes ONLY to its specific slice of state
const MatrixInput = ({ variantId }) => {
const qty = useMatrixStore((state) => state.quantities[variantId] || 0);
const setQuantity = useMatrixStore((state) => state.setQuantity);
return (
<input
value={qty}
onChange={(e) => setQuantity(variantId, parseInt(e.target.value))}
/>
);
}
This ensures that typing in one cell only re-renders that specific cell (and perhaps the “Total” counter), rather than the entire grid.
Phase 3: Micro-interactions & UX
Speed is technical, but it is also perceptual. A B2B buyer expects the tool to behave like a spreadsheet.
Keyboard Navigation
A mouse-heavy interface is too slow for bulk entry. We must implement Arrow Key Navigation.
- Enter: Move Down (Spreadsheet standard).
- Tab: Move Right.
- Arrow Keys: Move in respective directions.
This requires managing focus programmatically. Since we are virtualizing, the input you want to focus on might not exist in the DOM yet. This is the tricky part. You must scroll the virtualizer to the index before attempting to focus.
Instant Inventory Feedback
Users shouldn’t wait for “Add to Cart” to know a variant is out of stock. We pre-fetch inventory data in a lightweight format:
{
"variant_1": 50,
"variant_2": 0,
"variant_3": 1200
}
We map this to a visual state.
- Grayed Out: Out of Stock (0).
- Yellow Warning: Low Stock (User typed 50, Stock is 40).
- Red Border: Invalid Input.
This validation must happen synchronously on the client side.
Phase 4: API Payload & Batching
The user clicks “Add to Order”. They have selected 150 unique variants. Most REST APIs (including Shopify’s standard Cart API) are not designed to ingest 150 line items in a single HTTP POST request efficiently. It may timeout, or exceed payload limits.
Strategy: The Batched Promise Queue
We never block the UI. We show a progress bar (“Adding items… 40%”).
const BATCH_SIZE = 50;
async function addToCartRecursive(items: Item[]) {
if (items.length === 0) return;
const chunk = items.slice(0, BATCH_SIZE);
const remaining = items.slice(BATCH_SIZE);
// Optimistic UI Update here
try {
await api.cart.add(chunk);
updateProgress((total - remaining.length) / total);
return addToCartRecursive(remaining); // Next batch
} catch (error) {
handlePartialFailure(chunk, error);
}
}
Strategy: The Cart Transform (Shopify)
For Shopify Plus merchants, we utilize Cart Transform Functions or Bundles. We can add a single parent item (“The Matrix Bundle”) to the cart, and let backend logic expand it into line items at checkout. This keeps the cart interactions blazing fast while preserving backend logic for fulfillment. See our guide on Checkout Extensibility for more on this.
Mobile: The Pivot
A 50x20 grid is impossible on mobile. Do not try to make it responsive by shrinking the cells. It is unusable.
On mobile, we Pivot the UI.
Instead of Rows = Sizes and Cols = Colors, we simply show the Primary Attribute (e.g., Color) as a list.
- User taps “Red”.
- An accordion expands (or a bottom sheet opens).
- User sees a list of Sizes for “Red”.
- User inputs quantities.
- User collapses “Red” and taps “Blue”.
This “Drill-down” approach respects the small screen real estate while keeping the data hierarchy intact.
Performance Benchmarks
When we migrated a major client from a standard React form to a Virtualized Zustand Matrix, we observed:
| Metric | Legacy Grid (Standard React) | Maison Code Matrix (Virtualized) | Improvement |
|---|---|---|---|
| Initial Render | 3.2s | 0.25s | 12x Faster |
| Input Latency | 150ms | < 16ms (60fps) | 10x Faster |
| Memory Usage | 450MB | 60MB | 7x Leaner |
| Lighthouse Score | 42 | 98 | Green |
Conclusion
Building a Matrix Ordering interface is the litmus test for B2B frontend engineering. It forces you to confront the limits of the browser and the limits of your framework. It separates the “React Developers” from the “Software Engineers.”
At Maison Code, we believe that B2B buyers deserve the same fluid, delightful experience as B2C shoppers. Just because it’s “enterprise” doesn’t mean it has to be slow.
When you respect the user’s time by saving them milliseconds on every interaction, you aren’t just writing code—you are building revenue.
Ready to Optimize your B2B Experience?
If your wholesale portal feels sluggish, your buyers are ordering less.