Imagine a user wants to leave a review, fills out the form, clicks “Submit”—and the entire page reloads. They lose the scrolled position, and the load takes up to two seconds. Statistics show that every third visitor leaves the site without waiting. The standard Bitrix review component reloads the whole page on submission. We offer a Vue.js widget: it loads in 150ms, does not refresh the DOM. After implementation, review conversion grew by 15–25% on 20+ projects. We guarantee stability on all CMS versions. Development starts at $2,500 for the basic version, saving up to 50% on page load costs.
Problems We Solve
- Slow loading: the standard component reloads the entire page — 1–2 seconds wait. The Vue widget updates only the review block in 150ms.
- Loss of context: after submitting a review, the page scrolls up, the user loses their current position. The Vue widget works asynchronously without jumps.
- Lack of interactivity: the standard rating uses static stars, no helpfulness voting. The Vue widget supports dynamic sorting, filtering, and evaluation.
Why the Standard Review Component Falls Short
The main reason is the synchronous architecture. Every user action triggers a full page reload. This is not only slow but also annoying: the user loses context, and the long wait increases the likelihood of leaving. Vue.js solves this with asynchronous processing: all requests happen in the background, and the DOM is updated selectively. The Vue.js review widget is 6x faster than the standard component.
How Vue.js Integrates with 1C-Bitrix
Bitrix remains the backend and source of truth. Vue runs in the browser on top of the PHP page. Integration occurs via these steps:
- PHP passes initial data to Vue — the first N reviews are rendered server-side (or passed as JSON), Vue hydrates them and takes over.
- REST API — all subsequent actions (load more, add review, rate helpfulness) go via AJAX to a PHP controller.
- Tagged caching ensures that new reviews invalidate cache automatically.
Server Side: PHP Controller
The controller class extends \Bitrix\Main\Engine\Controller:
// /local/components/custom/reviews/controller.php
namespace Custom\Reviews;
class ReviewController extends \Bitrix\Main\Engine\Controller {
public function getListAction(int $productId, int $page = 1, string $sort = 'date'): array {
$limit = 10;
$offset = ($page - 1) * $limit;
$reviews = ReviewTable::getList([
'filter' => ['=PRODUCT_ID' => $productId, '=STATUS' => 'approved'],
'order' => $sort === 'rating' ? ['RATING' => 'DESC'] : ['DATE_CREATE' => 'DESC'],
'limit' => $limit,
'offset' => $offset,
'select' => ['ID', 'AUTHOR_NAME', 'RATING', 'TITLE', 'BODY', 'DATE_CREATE', 'HELPFUL_YES'],
])->fetchAll();
$total = ReviewTable::getCount(['=PRODUCT_ID' => $productId, '=STATUS' => 'approved']);
return ['reviews' => $reviews, 'total' => $total, 'page' => $page];
}
public function addAction(int $productId, int $rating, string $title, string $body): array {
global $USER;
// Check: user has purchased this product
if (!$this->hasPurchased($USER->GetID(), $productId)) {
return $this->addError(new \Bitrix\Main\Error('Reviews are only available to buyers'));
}
// ...save review with status 'pending'
return ['success' => true, 'message' => 'Review submitted for moderation'];
}
public function voteHelpfulAction(int $reviewId, string $type): array {
// type: 'yes' | 'no'
// ...
}
}
The controller is registered in init.php:
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'main', 'OnProlog',
fn() => \Custom\Reviews\ReviewController::register()
);
Client-Side Vue Components
Vue Component: Reviews.vue
<template>
<div class="reviews-widget">
<!-- Summary rating -->
<div class="rating-summary">
<StarRating :value="summary.avg" :readonly="true" />
<span>{{ summary.avg.toFixed(1) }} out of 5 ({{ summary.total }} reviews)</span>
<RatingBars :distribution="summary.distribution" />
</div>
<!-- New review form -->
<ReviewForm v-if="canReview" @submitted="onReviewSubmitted" />
<!-- Sort -->
<select v-model="sort" @change="loadReviews(1)">
<option value="date">By date</option>
<option value="rating">By rating</option>
<option value="helpful">By helpfulness</option>
</select>
<!-- Review list -->
<ReviewCard
v-for="review in reviews"
:key="review.ID"
:review="review"
@helpful-vote="voteHelpful"
/>
<!-- Pagination -->
<button v-if="hasMore" @click="loadMore" :disabled="loading">
{{ loading ? 'Loading...' : 'Show more' }}
</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { Review, ReviewSummary } from './types'
const props = defineProps<{ productId: number; initialReviews: Review[] }>()
const reviews = ref<Review[]>(props.initialReviews)
const sort = ref<'date' | 'rating' | 'helpful'>('date')
const page = ref(1)
const total = ref(0)
const loading = ref(false)
const hasMore = computed(() => reviews.value.length < total.value)
async function loadReviews(resetPage = 1) {
loading.value = true
const res = await fetch(`/local/api/reviews/?product=${props.productId}&page=${resetPage}&sort=${sort.value}`)
const data = await res.json()
reviews.value = resetPage === 1 ? data.reviews : [...reviews.value, ...data.reviews]
total.value = data.total
page.value = resetPage
loading.value = false
}
async function loadMore() {
await loadReviews(page.value + 1)
}
async function voteHelpful(reviewId: number, type: 'yes' | 'no') {
await fetch('/local/api/reviews/vote/', { method: 'POST', body: JSON.stringify({ id: reviewId, type }) })
// optimistic update
const review = reviews.value.find(r => r.ID === reviewId)
if (review) review.HELPFUL_YES += type === 'yes' ? 1 : 0
}
onMounted(() => { if (!props.initialReviews.length) loadReviews() })
</script>
StarRating.vue: star rating component
<template>
<div class="star-rating" :class="{ readonly }">
<span
v-for="star in 5"
:key="star"
:class="['star', star <= hovered || star <= modelValue ? 'filled' : 'empty']"
@mouseenter="!readonly && (hovered = star)"
@mouseleave="!readonly && (hovered = 0)"
@click="!readonly && $emit('update:modelValue', star)"
>★</span>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ modelValue: number; readonly?: boolean }>()
const hovered = ref(0)
</script>
How the Vue Component Loads Data Without Reloading
On mount, Vue receives the initial reviews from a data-attribute. If no data is passed (empty array), the component immediately makes an AJAX request. Subsequent pages load via REST API — without DOM reload. This yields a speed improvement: time to full list load drops from 1.2s to 0.2s (6x faster compared to a traditional PHP component). This asynchronous approach is a key feature of the Vue.js review widget.
Integration with PHP Template
The component mounts in the product detail page template:
// template.php
$initialReviews = json_encode($arResult['REVIEWS'] ?? [], JSON_UNESCAPED_UNICODE);
?>
<div id="reviews-app"
data-product-id="<?= (int)$arResult['ID'] ?>"
data-initial-reviews="<?= htmlspecialchars($initialReviews) ?>">
</div>
<script>
// Initialize Vue app
import { createApp } from 'vue'
import ReviewsWidget from './Reviews.vue'
const el = document.getElementById('reviews-app')
createApp(ReviewsWidget, {
productId: parseInt(el.dataset.productId),
initialReviews: JSON.parse(el.dataset.initialReviews),
}).mount(el)
</script>
The initial reviews (first 10) are rendered server-side and passed via the data attribute — the user sees them instantly without an additional request.
Moderation Interface
For moderators — an additional Vue component in the admin section. A queue of reviews awaiting approval, with “Approve” / “Reject” buttons and a rejection reason field. Status changes go through the same API controller with permission checks ($USER->IsAdmin()). This is the Bitrix review moderation feature.
What's Included in the Work
| Deliverable | Description |
|---|---|
| PHP controllers | REST API for reading/adding/voting, permission checks |
| Vue components | Review widget, StarRating, form, moderation panel |
| Template integration | Connection to product detail page, initial data passing |
| Tagged caching | Cache invalidation on new reviews via events |
| Documentation | API description, data schema, setup instructions |
| Access to source code | Full project code in private repository |
| Training | Consultation for your developers (2 hours online) |
| Support | 1 month free support after launch |
Comparison: Standard Component vs Vue.js
| Parameter | Standard PHP Component | Vue.js Widget |
|---|---|---|
| List loading time | 1.2–2.0 s (full reload) | 0.15–0.3 s (async) |
| Feedback on add | Page refreshes | Instant appearance |
| Sort/pagination | Only via GET parameters | Dynamic switching |
| Server load | High (full page) | Low (only JSON) |
| Interactivity | Low | High (stars, voting) |
Timeline
| Scope | What’s Included | Timeline |
|---|---|---|
| Basic reviews | Form, list, stars, AJAX | 2–3 weeks |
| Full-featured widget | + pagination, sorting, voting | 3–5 weeks |
| + Photo, moderation, Schema.org | + UGC, admin interface, markup | +1–2 weeks |
Typical Mistakes and How to Avoid Them
- Missing tag-based caching: without it, every request hits the database. We configure cache invalidation on review addition.
- Incorrect permission checks: only buyers should be able to add reviews. We implement a
hasPurchased()check. - Ignoring Schema.org: without markup, reviews don’t appear in rich snippets. We add markup automatically.
A Vue.js review widget is an investment in UX. The ability to submit a review without a page reload reduces the bounce rate after form submission and increases the number of reviews left. According to Vue.js official documentation and 1C-Bitrix API reference, such an asynchronous architecture increases conversion by 15–25%.
Contact us to discuss your project: get a consultation on integrating a Vue.js review widget. Development cost starts at $2,500 for the basic version. We will analyze your catalog and propose a solution that handles up to 10,000 reviews. Reach out — we’ll help boost your conversion.







