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

Наша компанія займається розробкою, підтримкою та обслуговуванням рішень на Бітрікс та Бітрікс24 будь-якої складності. Від простих односторінкових сайтів до складних інтернет-магазинів, CRM систем з інтеграцією 1С та телефонії. Досвід розробників підтверджено сертифікатами від вендора.
Пропоновані послуги
Показано 1 з 1 послугУсі 1626 послуг
Розробка каталогу товарів на React для 1С-Бітрікс
Середня
~1-2 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851
  • 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
    585
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Розробка на базі 1С Підприємство для компанії МИРСАНБЕЛ
    751
  • image_crm_dolbimby_434_0.webp
    Розробка сайту на CRM Бітрікс24 для компанії DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Розробка на базі Бітрікс24 для компанії ТЕХНОТОРГКОМПЛЕКС
    989

Розробка каталогу товарів на 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 секунд для серверного рендерингу з тим самим обсягом даних. Користувачі це відчувають, конверсія зростає.