Разработка каталога товаров на React для 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Разработка каталога товаров на React для 1С-Битрикс
Средняя
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1167
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Разработка на базе Битрикс, Битрикс24, 1С для компании Development of an Online Appointment Booking Widget for a Medical Center
    563
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    743
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Разработка каталога товаров на React для 1С-Битрикс

Каталог — самая нагруженная часть интернет-магазина: фильтрация, сортировка, пагинация, быстрый просмотр, добавление в корзину. Стандартный Битрикс-компонент bitrix:catalog с каждой фильтрацией делает полный перезапрос страницы. React-каталог — мгновенная реакция на фильтры, плавные переходы, бесконечная прокрутка. Разница в UX ощутима сразу.

API для каталога

Бэкенд-часть на PHP — API, который выдаёт данные каталога в JSON. Базовый контроллер:

// /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-компонент каталога с фильтрами

// 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>
    );
}

Фильтры синхронизируются с URL через useSearchParams — это позволяет поделиться ссылкой на конкретную фильтрацию и корректно работать с кнопкой «назад» в браузере.

Умный фильтр (фасетный поиск)

Стандартный компонент bitrix:catalog.smart.filter генерирует HTML. Для React-фильтра нужен 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;

Зависимые фильтры (когда выбор одного значения сужает доступные значения другого) — сложная задача. Реализуется через повторный запрос к API при изменении любого фильтра с передачей текущего выбора.

Оптимизация производительности

Виртуализация списка. При показе 100+ товаров используйте @tanstack/react-virtual — рендерится только видимая область:

import { useVirtual } from '@tanstack/react-virtual';

const rowVirtualizer = useVirtual({
    count: items.length,
    parentRef: containerRef,
    estimateSize: () => 350, // высота карточки товара
});

Prefetch следующей страницы. При скролле к последней видимой строке предзагружайте следующую страницу:

useEffect(() => {
    if (isNearEnd && data?.pages > filters.page) {
        queryClient.prefetchQuery(
            ['catalog', sectionId, { ...filters, page: filters.page + 1 }],
            () => fetchCatalog(sectionId, { ...filters, page: filters.page + 1 })
        );
    }
}, [isNearEnd]);

Изображения. Lazy loading через loading="lazy" + современные форматы (WebP через Битрикс \Bitrix\Main\Web\Uri + конвертер или внешний CDN). Для skeleton-заглушек при загрузке используйте CSS-анимацию — дешевле, чем JavaScript-анимации.

React-каталог с правильной архитектурой загружается за 200–400мс вместо 1.5–3 секунд для серверного рендеринга с тем же объёмом данных. Пользователи это чувствуют, конверсия растёт.