Decoupled Frontend Development for 1C-Bitrix
Decoupled means the frontend is physically separated from Bitrix: it lives on its own server, deploys independently, uses its own stack (React, Vue, Next.js), and communicates with Bitrix only through an API. Unlike a pure headless setup, decoupled can keep some pages on Bitrix templates — for example, the admin panel or CMS-generated pages.
When Decoupled Makes Sense
Decoupled is justified when the existing Bitrix template has become a bottleneck in performance or UX, but a full rewrite is not feasible right now. Typical scenarios:
- The product catalog is replaced with a React SPA while the rest of the pages stay on Bitrix templates
- The customer account area is rewritten in Vue while everything else remains unchanged
- A mobile app pulls data from Bitrix through an API while the website stays on the template
This is an evolutionary approach: you replace parts of the site incrementally, without a full redesign.
The "Island" Pattern — Partial Frontend Integration
Instead of fully separating the frontend, you embed React components into an existing Bitrix template through mount points:
// Inside the Bitrix catalog component template
// /local/templates/main/components/bitrix/catalog/my_template/template.php
// Pass data to React via a data attribute or global variable
$catalogData = json_encode($arResult['ITEMS']);
?>
<div id="react-catalog"
data-items="<?= htmlspecialchars($catalogData) ?>"
data-currency="RUB">
</div>
<script src="/local/js/dist/catalog.bundle.js"></script>
<script>
window.BitrixCatalog && window.BitrixCatalog.mount(
document.getElementById('react-catalog'),
<?= $catalogData ?>
);
</script>
// catalog.bundle.js — built by Webpack/Vite independently
import { createRoot } from 'react-dom/client';
import { CatalogApp } from './CatalogApp';
window.BitrixCatalog = {
mount(container, initialData) {
const root = createRoot(container);
root.render(<CatalogApp initialData={initialData} />);
}
};
This approach lets you develop the frontend in React with a full toolchain (TypeScript, hot reload, tests) without touching the rest of the Bitrix site.
Build Configuration (Vite)
// vite.config.js for decoupled Bitrix components
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: '../public/local/js/dist',
lib: {
entry: './src/index.tsx',
name: 'BitrixComponents',
formats: ['iife'], // IIFE for embedding in Bitrix templates
fileName: 'components',
},
rollupOptions: {
external: [], // do not externalize dependencies
},
},
server: {
cors: true, // allow CORS for the dev server
port: 3000,
},
});
In development mode the Vite dev server runs on localhost:3000 while the Bitrix site runs on site.local. API calls from the dev server are proxied through the Vite proxy:
server: {
proxy: {
'/api': {
target: 'http://site.local',
changeOrigin: true,
}
}
}
State Management Between Bitrix and React
The most complex part of decoupled is synchronizing state between the Bitrix part and React components. The cart is the classic example: the cart icon in the Bitrix header must show the correct item count as products are added through the React catalog.
Solution using a custom Event Bus:
// shared/eventBus.js — accessible from both the Bitrix part and React
window.BitrixEventBus = {
listeners: {},
emit(event, data) {
(this.listeners[event] || []).forEach(cb => cb(data));
},
on(event, callback) {
(this.listeners[event] ||= []).push(callback);
}
};
// In the React component when adding to cart:
window.BitrixEventBus.emit('cart:updated', { count: newCount });
// In the Bitrix template (header):
window.BitrixEventBus.on('cart:updated', ({ count }) => {
document.querySelector('.cart-counter').textContent = count;
});
Deployment and CI/CD
A decoupled frontend requires a dedicated build step in CI/CD. A simple GitLab CI pipeline:
build-frontend:
stage: build
script:
- cd frontend
- npm ci
- npm run build
- rsync -avz dist/ server:/var/www/site/local/js/dist/
only:
- main
After deploying the frontend, Bitrix templates automatically pick up the new bundle version. Version your files by content hash (catalog.a1b2c3.js) to avoid browser caching issues.
Decoupled is a sensible compromise between "rewrite everything" and "leave it as is." It lets you modernize the frontend incrementally, reduces risk, and gives you a realistic measure of ROI from the new stack before committing to a full migration.







