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.







