Catalog is the most loaded part of an online store: filtering, sorting, pagination, quick view, add to cart. The standard bitrix:catalog component reloads the entire page on each filter change with server-side rendering, taking 1.5–3 seconds. Imagine a catalog with 50,000 products — every filter click means wasted time and customer frustration. We develop a Single Page Application (SPA) React catalog that provides instant response to user actions: page loads in 200–400 ms, filter change takes up to 300 ms. The UX difference is immediate: cart conversion increases by 15–30%. Our experience: over 10 years and 40+ projects in catalog development for Bitrix. We are a certified Bitrix partner with thousands of satisfied clients, guaranteeing a performance improvement of at least 50%. For a store with $1M annual revenue, a 7% conversion drop due to slow load means $70,000 loss yearly; our React catalog recovers that.
Why React instead of a standard catalog?
Browser-side rendering eliminates full page reloads. Every filter change is an asynchronous request to the REST API, not a page reload. The backend uses tagged caching: when a product changes, only the cache associated with that product is invalidated, not the entire catalog. Result: smooth animations, filter state preserved in URL, the ability to share a link to specific search results. Users spend more time in the catalog, conversion increases. Our React 1C-Bitrix integration ensures seamless data exchange via CommerceML.
Catalog API Workflow
The PHP backend outputs catalog data in JSON. The base controller handles requests with filters, sorting, and pagination:
Code example: catalog list API
// /local/ajax/api.php — обработчик catalog.list
case 'catalog.list':
CModule::IncludeModule('iblock');
CModule::IncludeModule('catalog');
$filter = [
'IBLOCK_ID' => CATALOG_IBLOCK_ID,
'ACTIVE' => 'Y',
'SECTION_ID'=> (int)$_GET['section_id'],
];
// Ценовые фильтры
if (!empty($_GET['price_from'])) {
$filter['>=CATALOG_PRICE_1'] = (float)$_GET['price_from'];
}
if (!empty($_GET['price_to'])) {
$filter['<=CATALOG_PRICE_1'] = (float)$_GET['price_to'];
}
// Фильтр по свойствам
if (!empty($_GET['props'])) {
$props = json_decode($_GET['props'], true);
foreach ($props as $propCode => $values) {
$filter['PROPERTY_' . $propCode] = $values;
}
}
$sort = match($_GET['sort'] ?? 'default') {
'price_asc' => ['CATALOG_PRICE_1' => 'ASC'],
'price_desc' => ['CATALOG_PRICE_1' => 'DESC'],
'new' => ['DATE_CREATE' => 'DESC'],
default => ['SORT' => 'ASC'],
};
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = 24;
$res = CIBlockElement::GetList(
$sort, $filter, false,
['iNumPage' => $page, 'nPageSize' => $limit],
['ID', 'NAME', 'PREVIEW_PICTURE', 'DETAIL_PAGE_URL',
'PROPERTY_ARTICLE', 'CATALOG_PRICE_1']
);
$items = [];
while ($el = $res->GetNext()) {
$items[] = [
'id' => $el['ID'],
'name' => $el['NAME'],
'slug' => $el['CODE'],
'price' => (float)$el['CATALOG_PRICE_1'],
'image' => CFile::GetPath($el['PREVIEW_PICTURE']),
'url' => $el['DETAIL_PAGE_URL'],
];
}
echo json_encode([
'result' => $items,
'total' => $res->SelectedRowsCount(),
'pages' => ceil($res->SelectedRowsCount() / $limit),
]);
break;
React catalog component with filters
On the frontend we use the useSearchParams hook to synchronize filters with the URL. Each filter is a query string parameter, so the link to a specific result can be copied and shared with a colleague.
Code example: CatalogPage component
// CatalogPage.tsx
import { useState, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';
interface CatalogFilters {
priceFrom?: number;
priceTo?: number;
props: Record<string, string[]>;
sort: string;
page: number;
}
export function CatalogPage({ sectionId }: { sectionId: number }) {
const [searchParams, setSearchParams] = useSearchParams();
const filters: CatalogFilters = {
priceFrom: searchParams.get('price_from') ? Number(searchParams.get('price_from')) : undefined,
priceTo: searchParams.get('price_to') ? Number(searchParams.get('price_to')) : undefined,
props: JSON.parse(searchParams.get('props') || '{}'),
sort: searchParams.get('sort') || 'default',
page: Number(searchParams.get('page') || 1),
};
const { data, isLoading } = useQuery({
queryKey: ['catalog', sectionId, filters],
queryFn: () => fetchCatalog(sectionId, filters),
keepPreviousData: true,
});
const updateFilter = useCallback((key: string, value: string | null) => {
setSearchParams(prev => {
if (value) prev.set(key, value);
else prev.delete(key);
prev.delete('page');
return prev;
});
}, [setSearchParams]);
return (
<div className="catalog-layout">
<CatalogFilters
filters={filters}
onFilterChange={updateFilter}
/>
<div className="catalog-main">
<CatalogToolbar
total={data?.total}
sort={filters.sort}
onSortChange={v => updateFilter('sort', v)}
/>
{isLoading ? (
<ProductGrid items={Array(24).fill(null)} skeleton />
) : (
<ProductGrid items={data?.items || []} />
)}
<Pagination
current={filters.page}
total={data?.pages || 1}
onChange={p => updateFilter('page', String(p))}
/>
</div>
</div>
);
}
Filters are synchronized with the URL via useSearchParams — this allows sharing a link to a specific filter set and works correctly with the browser's back button.
Smart Filter Capabilities
The standard bitrix:catalog.smart.filter component generates HTML unsuitable for React. A custom filter requires a smart filter API:
// catalog.filter.get — доступные значения фильтров для текущего раздела
case 'catalog.filter.get':
// Получаем доступные свойства и их значения
// с учётом текущих выбранных фильтров (для зависимых фильтров)
$availableProps = getSmartFilterProps(
CATALOG_IBLOCK_ID,
(int)$_GET['section_id'],
json_decode($_GET['selected'] ?? '{}', true)
);
echo json_encode(['result' => $availableProps]);
break;
Dependent filters (where selecting one value narrows down available values of another) is a complex task. It is implemented via a repeated request to the API on any filter change, passing the current selection. This approach provides behavior similar to the standard smart filter but in SPA style. Additionally, we use HL-blocks to store user preferences, which speeds up return visits.
Optimal Time to Switch
If your catalog contains more than 10,000 products, has complex filters with dependencies, or you notice a conversion drop on mobile devices — a React catalog solves these problems. Speed directly affects revenue: every second of delay reduces conversion by 7%. Investment starts from $3,000 and can reach $15,000 for complex projects, with typical ROI achieved within 6 months. For a store with $1M annual revenue, that's $70,000 saved per year. A full-stack approach (PHP + React) gives flexibility when integrating with 1C via CommerceML.
Performance optimization
| Method | Server Rendering | React Catalog |
|---|---|---|
| First load | 1.5–3 s | 0.2–0.4 s |
| Filter change | 1.5–3 s (reload) | 0.1–0.3 s |
| Add to cart | 0.5–1 s | 0.1–0.2 s |
| State preservation | no | yes |
List virtualization. When displaying 100+ products, use @tanstack/react-virtual — only the visible area is rendered. This reduces memory consumption and speeds up scrolling.
import { useVirtual } from '@tanstack/react-virtual';
const rowVirtualizer = useVirtual({
count: items.length,
parentRef: containerRef,
estimateSize: () => 350,
});
Prefetch next page. When scrolling near the last visible item, prefetch the next page:
useEffect(() => {
if (isNearEnd && data?.pages > filters.page) {
queryClient.prefetchQuery(
['catalog', sectionId, { ...filters, page: filters.page + 1 }],
() => fetchCatalog(sectionId, { ...filters, page: filters.page + 1 })
);
}
}, [isNearEnd]);
Image lazy loading via loading="lazy" + modern formats (WebP through Bitrix \Bitrix\Main\Web\Uri + converter or external CDN). For skeleton placeholders during loading, use CSS animation — cheaper than JavaScript animations.
Development stages of a React catalog
Checklist for implementation:
- Analysis of information blocks and properties, API design.
- Development of API controllers (list, filters, pagination).
- Creation of React components (filter, grid, pagination, skeletons).
- Integration with the trade catalog and 1C exchange via CommerceML.
- Configuration of tagged caching and HTTP cache.
- Testing on real data, performance measurements.
- Preparation of documentation and training of the customer's team.
The process includes analysis, API design, frontend and backend development, 1C integration, and testing. Each stage ends with a demo to the customer.
| Stage | Duration | Result |
|---|---|---|
| Analysis | 3–5 days | Technical specification and API prototype |
| API development | 5–10 days | Ready endpoints |
| Frontend | 7–14 days | Working catalog with filters |
| Integration | 3–5 days | Full compatibility with 1C |
| Testing | 2–4 days | Performance report |
What is included in the work
Deliverables:
- API controllers for product list, filters, pagination.
- React components: filter, product grid, pagination, skeletons.
- Integration with existing information blocks, trade catalog, 1C.
- Caching configuration (tagged caching, HTTP cache).
- API and architecture documentation.
- Git repository access with full code.
- Training of your developers (up to 2 hours) and 2 months of email support.
Get a free consultation on your project — we'll assess the scope and propose the optimal solution. Order the development of a React catalog and boost your store's conversion.







