SPA development using React for 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

React SPA Development for 1C-Bitrix

A Single Page Application on top of 1C-Bitrix means the browser loads a single HTML page and all subsequent navigation happens without a full reload. Bitrix serves data through an API, and React Router manages URLs and transitions. This suits customer account areas, B2B portals, and admin interfaces — anywhere users work in the system for extended periods and a page reload destroys the UX.

SPA Structure and Entry Point

The SPA is mounted through a single entry point in a Bitrix template:

// /local/templates/main/components/bitrix/system.auth.form/lk/template.php
// Or a dedicated page /lk/index.php

defined('B_PROLOG_INCLUDED') && (B_PROLOG_INCLUDED === true) || die();
?>
<!DOCTYPE html>
<html>
<head>
    <title>Account Area</title>
    <?php $APPLICATION->ShowHead(); ?>
</head>
<body>
    <!-- Initial data for SPA bootstrap -->
    <script>
        window.__INITIAL_STATE__ = <?= json_encode([
            'user'     => $arResult['USER'],
            'csrfToken'=> bitrix_sessid(),
            'apiBase'  => '/local/ajax/',
        ]) ?>;
    </script>

    <div id="spa-root"></div>

    <?php $APPLICATION->ShowFooter(); ?>
    <script type="module" src="/local/js/dist/lk.js"></script>
</body>
</html>

React Router: SPA Routing

// /local/js/src/lk/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';

export function App() {
    const { data: auth } = useAuth();

    if (!auth?.isAuthorized) {
        // Redirect to Bitrix standard login page
        window.location.href = '/auth/?backurl=' + encodeURIComponent(window.location.pathname);
        return null;
    }

    return (
        <BrowserRouter basename="/lk">
            <AppLayout>
                <Routes>
                    <Route index element={<Dashboard />} />
                    <Route path="orders" element={<Orders />} />
                    <Route path="orders/:id" element={<OrderDetail />} />
                    <Route path="profile" element={<Profile />} />
                    <Route path="*" element={<Navigate to="/" replace />} />
                </Routes>
            </AppLayout>
        </BrowserRouter>
    );
}

Important: use basename in BrowserRouter so React Router knows the base path and does not break navigation.

State Management: Zustand

For a SPA of moderate complexity, Zustand is a better choice than Redux:

// /local/js/src/lk/store/cartStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { bitrixApi } from '../api/bitrix';

interface CartState {
    items: CartItem[];
    total: number;
    addItem: (productId: number, quantity: number) => Promise<void>;
    removeItem: (itemId: number) => Promise<void>;
    syncWithServer: () => Promise<void>;
}

export const useCartStore = create<CartState>()(
    persist(
        (set, get) => ({
            items: [],
            total: 0,

            addItem: async (productId, quantity) => {
                const result = await bitrixApi.post<{ items: CartItem[]; total: number }>(
                    'cart.add',
                    { product_id: productId, quantity }
                );
                set({ items: result.items, total: result.total });
            },

            removeItem: async (itemId) => {
                const result = await bitrixApi.post<{ items: CartItem[]; total: number }>(
                    'cart.remove',
                    { item_id: itemId }
                );
                set({ items: result.items, total: result.total });
            },

            syncWithServer: async () => {
                const result = await bitrixApi.get<{ items: CartItem[]; total: number }>('cart.get');
                set({ items: result.items, total: result.total });
            },
        }),
        { name: 'bitrix-cart' }
    )
);

Server-Side Rendering (SSR) — Is It Needed?

A client-rendered SPA has one well-known drawback: search engine bots struggle to index content generated by JavaScript. For a customer account area (a private section) this is irrelevant. For a public catalog it is critical.

If the SPA includes public pages, SSR is required. Options:

  • Next.js with server-side API requests to Bitrix — the cleanest approach
  • Vite SSR — more complex to configure, but does not require switching frameworks
  • Static pre-rendering (SSG) for pages with infrequently changing content

For private sections (customer accounts, B2B portals) SSR is unnecessary — save yourself the complexity.

Error Handling and Offline Mode

A SPA must handle network and API errors gracefully:

// Global Error Boundary
class ApiErrorBoundary extends Component<Props, State> {
    state = { hasError: false, error: null };

    static getDerivedStateFromError(error: Error) {
        return { hasError: true, error };
    }

    render() {
        if (this.state.hasError) {
            return <ErrorScreen error={this.state.error} onRetry={() =>
                this.setState({ hasError: false })} />;
        }
        return this.props.children;
    }
}

For offline resilience, a Service Worker caches GET requests to the API. When the connection is lost, the user sees cached data with an "offline mode" indicator, while POST requests are queued and executed when connectivity is restored.

A SPA in a Bitrix project demands discipline at the boundary between the two systems: Bitrix must provide data through an API, React must only render and handle input. As soon as business logic starts being duplicated — part in PHP components, part in JS — the project becomes unreadable. Keep the boundary clear.