Олег Марков
Вычисляемые свойства computed во Vue.js
Введение
Вычисляемые свойства (computed) во Vue.js — это один из тех инструментов, которые сильно упрощают жизнь, если правильно ими пользоваться. Они позволяют описывать производные значения на основе состояния компонента и при этом не дублировать код в шаблоне или в методах.
Смотрите, идея простая: у вас есть какие‑то данные, а вам нужно на их основе получить другие значения, которые автоматически пересчитываются, когда исходные данные меняются. Вместо того чтобы вручную вызывать функции при каждом изменении, вы описываете вычисляемое свойство — и Vue сам следит за зависимостями и обновляет результат.
Давайте разберемся, что такое computed, чем оно отличается от методов и наблюдателей (watch), как правильно его использовать и какие подводные камни чаще всего возникают у разработчиков.
Что такое вычисляемые свойства computed
Основная идея computed
Вычисляемое свойство — это по сути «виртуальное» поле, значение которого считается на основе других реактивных данных компонента. Главное отличие от обычного метода в том, что результат вычисляемого свойства кэшируется до тех пор, пока не изменятся его зависимости.
Проще говоря:
- computed ведет себя как обычное свойство (обращение без скобок);
- значение автоматически обновляется, когда меняются зависимые данные;
- между изменениями значение берется из кэша, а не пересчитывается каждый раз.
Простейший пример
Давайте посмотрим на базовый пример на Vue 2:
// Здесь мы создаем новый экземпляр Vue
const vm = new Vue({
el: '#app',
data: {
firstName: 'Иван',
lastName: 'Иванов'
},
computed: {
// Здесь мы объявляем вычисляемое свойство fullName
fullName() {
// Оно будет автоматически пересчитываться,
// когда изменится firstName или lastName
return this.firstName + ' ' + this.lastName
}
}
})
В шаблоне это выглядит так:
<div id="app">
<!-- Здесь мы обращаемся к computed-свойству как к обычному полю -->
<p>{{ fullName }}</p>
</div>
Обратите внимание: мы пишем {{ fullName }}, а не {{ fullName() }}. Это важно — вычисляемые свойства работают как геттеры, а не как методы.
Теперь, если вы сделаете:
// Здесь мы меняем исходные данные
vm.firstName = 'Петр'
// Vue автоматически пересчитает fullName
Содержимое {{ fullName }} в шаблоне обновится автоматически.
Пример на Vue 3 (Composition API)
В Vue 3 есть отдельная функция computed, но идея та же. Смотрите, как это выглядит:
import { ref, computed } from 'vue'
export default {
setup() {
// Создаем реактивные данные через ref
const firstName = ref('Иван')
const lastName = ref('Иванов')
// Создаем вычисляемое свойство
const fullName = computed(() => {
// Используем .value, так как это ref
return firstName.value + ' ' + lastName.value
})
// Возвращаем, чтобы использовать в шаблоне
return {
firstName,
lastName,
fullName
}
}
}
В шаблоне будет то же самое:
<p>{{ fullName }}</p>
Здесь Vue сам следит, что fullName зависит от firstName и lastName, и пересчитывает его только когда эти значения меняются.
Чем computed отличается от methods и watch
Computed vs methods
Частый вопрос: почему бы просто не использовать метод? Давайте разберемся на простом примере.
computed: {
// Вычисляемое свойство
fullNameComputed() {
console.log('computed выполняется')
return this.firstName + ' ' + this.lastName
}
},
methods: {
// Метод
fullNameMethod() {
console.log('method выполняется')
return this.firstName + ' ' + this.lastName
}
}
И шаблон:
<p>{{ fullNameComputed }}</p>
<p>{{ fullNameMethod() }}</p>
<p>{{ fullNameMethod() }}</p>
Что происходит:
fullNameComputedвычислится один раз и закэшируется, пока не изменитсяfirstNameилиlastName;fullNameMethod()выполнится каждый раз при любом обновлении компонента и каждый раз, когда он вызван в шаблоне.
То есть:
- computed — про зависимые данные и кэширование;
- methods — про действия (обработчики, события, логика без кэша).
Если значение может быть однозначно вычислено из других реактивных данных и не зависит от внешних побочных эффектов (запросы, таймеры и т.п.), лучше использовать computed.
Computed vs watch
watch используется, когда вам нужно «реагировать» на изменение значения и выполнить побочный эффект: запрос на сервер, логирование, изменение нескольких полей сразу.
Пример на watch:
data() {
return {
searchQuery: '',
results: []
}
},
watch: {
// Здесь мы следим за изменением searchQuery
searchQuery(newValue) {
// Здесь мы вызываем функцию, которая делает запрос на сервер
this.fetchResults(newValue)
}
},
methods: {
fetchResults(query) {
// Здесь могла бы быть логика HTTP-запроса
console.log('Запрашиваем результаты для', query)
}
}
Здесь watch уместен, потому что:
- нам нужен побочный эффект — запрос;
- нам не нужно «виртуальное» значение для шаблона.
Если бы результаты можно было вычислить локально из других полей без побочных эффектов, удобнее было бы использовать computed.
Кратко:
- computed — для декларативных зависимых значений;
- watch — для побочных эффектов при изменении данных;
- methods — для действий, которые выполняются по событию и не кэшируются.
Кэширование и отслеживание зависимостей
Как работает кэширование вычисляемых свойств
Vue запоминает (кэширует) результат вычисляемого свойства и список зависимостей, от которых оно зависит. Когда вы обращаетесь к computed:
- если ни одна зависимость не изменилась — возвращается старое значение из кэша;
- если хотя бы одна зависимость изменилась — функция вычисления запускается заново.
Давайте посмотрим на пример, где кэширование помогает:
computed: {
heavyComputed() {
// Это "тяжелая" функция с большим количеством вычислений
console.log('Выполняем тяжелое вычисление...')
let sum = 0
// Здесь мы специально делаем много итераций
for (let i = 0; i < 1000000; i++) {
sum += i
}
return sum + this.extra
}
},
data() {
return {
extra: 10,
counter: 0
}
},
methods: {
increment() {
// Изменяем только counter
this.counter++
}
}
В шаблоне:
<p>{{ heavyComputed }}</p>
<p>{{ counter }}</p>
<button @click="increment">Увеличить</button>
Как это работает:
- при первом рендере
heavyComputedвыполнится и результат будет сохранен в кэше; - при клике на кнопку обновится только
counter; - зависимости
heavyComputed— этоextra, а неcounter, поэтому тяжелая функция не будет запускаться снова; - Vue возьмет закэшированное значение.
Именно поэтому вычисляемые свойства особенно полезны, когда вычисления дорогие по времени.
Как Vue понимает, что пересчитывать
Vue автоматически отслеживает, к каким реактивным данным вы обращаетесь внутри функции computed. Все такие обращения добавляются в список зависимостей. Если какое‑то из этих значений изменится, вычисляемое свойство помечается как «грязное», и при следующем доступе к нему будет выполнен пересчет.
То есть вам не нужно явно указывать зависимости, как, например, в React с useMemo — Vue делает это за вас.
Геттеры и сеттеры для computed
Вычисляемое свойство только с геттером
Чаще всего computed используется как только геттер, без возможности записи:
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
Если вы попытаетесь присвоить этому свойству значение:
this.fullName = 'Новая строка'
Ничего полезного не произойдет, потому что по умолчанию setter у computed нет.
Computed с геттером и сеттером
Иногда бывает нужно «обратное» преобразование: вы хотите, чтобы при установке значения в computed автоматически менялись исходные поля. Тогда вы можете объявить computed как объект с полями get и set.
Давайте разберемся на примере:
data() {
return {
firstName: 'Иван',
lastName: 'Иванов'
}
},
computed: {
fullName: {
// Геттер вызывается, когда вы обращаетесь к this.fullName
get() {
return this.firstName + ' ' + this.lastName
},
// Сеттер вызывается, когда вы присваиваете this.fullName = '...'
set(value) {
// Здесь мы разбиваем строку на части по пробелу
const parts = value.split(' ')
// Обновляем исходные данные
this.firstName = parts[0] || ''
this.lastName = parts[1] || ''
}
}
}
Теперь вы можете сделать:
// Здесь сработает setter полноты имени
this.fullName = 'Петр Петров'
// firstName станет 'Петр', lastName станет 'Петров'
В шаблоне:
<!-- Здесь мы двусторонне связываем input с computed через v-model -->
<input v-model="fullName">
Обратите внимание:
- при вводе текста в поле ввода вызывается setter
fullName, он обновляетfirstNameиlastName; - при изменении
firstNameилиlastNameVue пересчитает значение fullName через getter, и это отразится в поле ввода.
Так вы получаете реактивное «виртуальное» поле, у которого есть и чтение, и запись.
Computed с get/set в Vue 3 (Composition API)
В Composition API вы можете задать геттер и сеттер через объект:
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('Иван')
const lastName = ref('Иванов')
const fullName = computed({
// Геттер
get() {
return firstName.value + ' ' + lastName.value
},
// Сеттер
set(value) {
const parts = value.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts[1] || ''
}
})
return {
firstName,
lastName,
fullName
}
}
}
Теперь в шаблоне можно использовать:
<input v-model="fullName">
Vue сам поймет, что это computed с геттером и сеттером.
Правильные сценарии использования computed
Когда использовать computed
Используйте вычисляемые свойства, когда:
- Значение логически является производным от других реактивных данных.
- Вам нужно использовать это значение в нескольких местах (в шаблоне или в логике).
- Вы хотите избежать дублирования кода.
- Вычисление может быть не самым дешевым по производительности, и кэширование имеет смысл.
Примеры:
- форматирование даты или числа;
- фильтрация и сортировка списков;
- подсчет сумм, количества, средних значений;
- объединение нескольких полей в одно (как в примере с fullName).
Когда лучше использовать методы
Предпочтительнее использовать methods, если:
- значение не зависит напрямую от реактивного состояния компонента;
- вычисление может быть разным при каждом вызове (например, случайные числа);
- результат не нужно кэшировать;
- функция больше похожа на действие, чем на «свойство».
Например:
methods: {
// Здесь мы генерируем случайный идентификатор
generateId() {
return Math.random().toString(36).slice(2)
}
}
Такую функцию нет смысла делать computed, потому что:
- кэширование ломает идею «случайности»;
- значение зависит не от данных, а от момента вызова.
Когда использовать watch
Watch полезен, когда:
- вы хотите выполнить побочный эффект при изменении значения;
- у вас есть асинхронная логика, которая не может быть «чистым» вычислением;
- нужно «поддерживать в актуальном состоянии» что‑то вне шаблона (локальное хранилище, URL, состояние в сторонней библиотеке).
Например:
watch: {
// Отслеживаем изменение выбранной вкладки
currentTab(newTab) {
// Сохраняем выбор пользователя в localStorage
// Это побочный эффект, а не просто вычисление значения
localStorage.setItem('lastTab', newTab)
}
}
Здесь computed не подходит, потому что сама суть — побочный эффект, а не новое «виртуальное» поле.
Примеры практического использования computed
Фильтрация списка
Давайте разберемся на примере типичной задачи: фильтрация списка элементов по строке поиска.
data() {
return {
search: '',
items: [
{ id: 1, name: 'Яблоко' },
{ id: 2, name: 'Банан' },
{ id: 3, name: 'Апельсин' }
]
}
},
computed: {
// Вычисляем отфильтрованный список
filteredItems() {
// Приводим строку к нижнему регистру для удобства поиска
const query = this.search.toLowerCase()
// Фильтруем элементы по условию вхождения подстроки
return this.items.filter(item =>
item.name.toLowerCase().includes(query)
)
}
}
Шаблон:
<input v-model="search" placeholder="Поиск">
<ul>
<!-- Здесь мы выводим только отфильтрованные элементы -->
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</li>
</ul>
Что здесь важно:
- filteredItems всегда «свежий» — он зависит от
searchиitems; - при вводе в поле поиска список автоматически пересчитывается;
- если список большой, кэширование помогает не пересчитывать его, когда меняется что‑то другое в компоненте.
Подсчет агрегированных значений
Представим корзину магазина. Покажу вам пример, как удобно посчитать итоговую сумму и количество товаров через computed.
data() {
return {
cart: [
{ id: 1, name: 'Товар A', price: 1000, quantity: 2 },
{ id: 2, name: 'Товар B', price: 500, quantity: 1 }
],
discount: 0.1 // Скидка 10%
}
},
computed: {
// Общее количество товаров
totalItems() {
return this.cart.reduce((sum, item) => {
// Складываем количество каждого товара
return sum + item.quantity
}, 0)
},
// Сумма без скидки
subtotal() {
return this.cart.reduce((sum, item) => {
// Складываем цену * количество
return sum + item.price * item.quantity
}, 0)
},
// Итоговая сумма с учетом скидки
total() {
// Используем другое computed-свойство
const discounted = this.subtotal * (1 - this.discount)
// Округляем до двух знаков после запятой
return Math.round(discounted * 100) / 100
}
}
Шаблон:
<p>Товаров в корзине - {{ totalItems }}</p>
<p>Сумма без скидки - {{ subtotal }} ₽</p>
<p>Итого со скидкой - {{ total }} ₽</p>
Обратите внимание:
- computed может зависеть от других computed (total зависит от subtotal);
- при изменении количества или скидки все итоговые значения пересчитаются автоматически.
Форматирование данных для отображения
Иногда в data вы храните «сырые» значения, а в шаблоне нужно показывать их в удобочитаемом виде. Здесь computed очень кстати.
data() {
return {
price: 1234.567,
createdAt: '2023-07-01T12:34:56Z'
}
},
computed: {
formattedPrice() {
// Здесь мы форматируем число как валюту
return this.price.toFixed(2) + ' ₽'
},
formattedDate() {
// Здесь мы создаем объект даты из строки
const date = new Date(this.createdAt)
// Собираем простую строку ДД.ММ.ГГГГ
const day = String(date.getDate()).padStart(2, '0')
const month = String(date.getMonth() + 1).padStart(2, '0')
const year = date.getFullYear()
return `${day}.${month}.${year}`
}
}
Шаблон:
<p>Цена - {{ formattedPrice }}</p>
<p>Создано - {{ formattedDate }}</p>
Смотрите, здесь мы отделяем «данные» (price, createdAt) от «представления» (formattedPrice, formattedDate). Это делает код чище и удобнее для поддержки.
Computed в Options API и Composition API
Вариант с Options API (Vue 2 и Vue 3)
В Options API все вычисляемые свойства описываются в одноименном объекте computed в опциях компонента:
export default {
data() {
return {
a: 1,
b: 2
}
},
computed: {
sum() {
return this.a + this.b
}
}
}
Плюсы:
- код хорошо читается для небольших компонентов;
- блок computed визуально отделен от методов и данных.
Минусы:
- при больших компонентах логика может быть «размазана» по разным опциям (data, computed, methods и т.д.).
Вариант с Composition API (Vue 3)
В Composition API вы используете функцию computed внутри setup:
import { ref, computed } from 'vue'
export default {
setup() {
const a = ref(1)
const b = ref(2)
const sum = computed(() => {
return a.value + b.value
})
return {
a,
b,
sum
}
}
}
Плюсы:
- можно группировать связанную логику рядом (данные, computed, watch, методы);
- удобнее масштабировать большие компоненты;
- проще выносить повторяемую логику в отдельные функции / композиционные хуки.
Особенность: с ref вам нужно использовать .value внутри computed. В шаблоне же sum и a ведут себя как обычные значения — Vue сам «разворачивает» .value.
Чего делать с computed не стоит
Не использовать побочные эффекты внутри computed
Вычисляемые свойства должны быть «чистыми» функциями — без побочных эффектов. То есть:
- никакого ручного изменения других реактивных данных;
- никаких HTTP-запросов;
- никаких таймеров, логирования в консоль в реальной логике и т.п.
Неправильный пример:
computed: {
userData() {
// Плохо - здесь мы делаем запрос
this.fetchUser() // Это метод, который меняет состояние
return this.user
}
}
Почему это опасно:
- computed может вызываться неоднократно, и вы получите кучу повторных запросов;
- вы смешиваете «вычисление значения» и «побочный эффект»;
- логика становится сложно предсказуемой.
Вместо этого:
- для запроса — используйте метод + вызов в mounted или по событию;
- для реакции на изменение значения — используйте watch.
Не мутировать состояния в computed
Неправильный (и потенциально опасный) пример:
computed: {
normalizedItems() {
// Плохо - мы мутируем исходный массив items
this.items.forEach(item => {
item.normalizedName = item.name.toLowerCase()
})
return this.items
}
}
Это создаст скрытые побочные эффекты — вы вроде бы считаете значение, а на самом деле меняете state.
Лучше сделать так:
computed: {
normalizedItems() {
// Хорошо - мы создаем новый массив и новый объект для каждого элемента
return this.items.map(item => {
return {
...item,
normalizedName: item.name.toLowerCase()
}
})
}
}
Здесь computed возвращает новое значение, не меняя исходные данные напрямую.
Не использовать computed там, где подходит простое поле data
Иногда разработчики делают computed просто ради факта, хотя это обычное состояние:
computed: {
// Не нужно
isVisible: {
get() {
return this._isVisible
},
set(value) {
this._isVisible = value
}
}
},
data() {
return {
_isVisible: false
}
}
Это усложнение без пользы. Если значение не является производным, его проще и понятнее держать в data:
data() {
return {
isVisible: false
}
}
Оптимизация и производительность
Когда computed реально помогает по скорости
Особенно полезно использовать computed в таких ситуациях:
- фильтрация или сортировка больших списков;
- сложные математические вычисления;
- преобразование данных, которые часто используются в шаблоне.
Например, если у вас таблица с сотнями строк и сложной логикой фильтрации, давайте вынесем ее в computed:
computed: {
processedRows() {
// Здесь мы последовательно применяем фильтрацию и сортировку
let rows = this.rows
if (this.search) {
const query = this.search.toLowerCase()
rows = rows.filter(row =>
row.name.toLowerCase().includes(query)
)
}
if (this.sortField) {
rows = [...rows].sort((a, b) => {
// Здесь мы сортируем по выбранному полю
if (a[this.sortField] < b[this.sortField]) return -1
if (a[this.sortField] > b[this.sortField]) return 1
return 0
})
}
return rows
}
}
Vue выполнит эту функцию только тогда, когда изменятся rows, search или sortField, а не при каждом обновлении компонента.
Когда кэширование не дает выгоды
Если вычисление очень простое и дешевое (например, суммирование двух чисел), кэширование дает минимальную выгоду, но все равно улучшает читаемость кода. Главное — не использовать computed для того, что не зависит от реактивных данных вообще.
Нецелесообразно, например, такое:
computed: {
currentTimestamp() {
// Здесь мы просто возвращаем текущее время
return Date.now()
}
}
Значение не зависит от состояния и становится устаревшим уже через миллисекунду, а кэширование только вредит. Для этого больше подходит метод или явное обновление значения в data через таймер.
Частые ошибки при работе с computed
Ошибка 1. Попытка передать аргументы в computed
Иногда разработчики пытаются сделать так:
computed: {
getUserById(id) {
// Так нельзя - computed не принимает аргументы
return this.users.find(user => user.id === id)
}
}
Computed не принимает аргументы. Это не метод, а свойство.
Как правильно:
- если нужен поиск по параметру — используйте метод;
- если нужно часто работать с конкретным параметром — сделайте отдельное computed или используйте computed, который возвращает функцию.
Пример с методом:
methods: {
getUserById(id) {
// Здесь мы ищем пользователя по id
return this.users.find(user => user.id === id)
}
}
Пример с computed, возвращающим функцию:
computed: {
userById() {
// Здесь мы возвращаем функцию, которая может принимать id
return id => this.users.find(user => user.id === id)
}
}
В шаблоне:
<p>{{ userById(10).name }}</p>
Ошибка 2. Отсутствие return в computed
Иногда забывают вернуть значение:
computed: {
fullName() {
// Здесь нет return
this.firstName + ' ' + this.lastName
}
}
Результат: в шаблоне fullName будет undefined. Всегда проверяйте, что в computed есть оператор return.
Ошибка 3. Использование async/await в computed
Vue поддерживает асинхронные функции в computed, но на практике это приводит к путанице. Например:
computed: {
async userData() {
// Здесь мы делаем асинхронный запрос
const response = await fetch('/api/user')
return await response.json()
}
}
Что пойдет не так:
- computed вернет Promise, а не готовые данные;
- шаблон не умеет «дождаться» результата сам по себе;
- придется вручную разворачивать Promise и писать дополнительную логику.
Чаще всего лучше:
- делать запросы в хуках (mounted, onMounted) или методах;
- сохранять результат в data / ref;
- использовать computed только для производных значений от уже загруженных данных.
Ошибка 4. Опора на то, что computed будет вызываться строго в определенный момент
Не стоит полагаться на порядок и частоту вызовов computed. Vue сам решает, когда и сколько раз вызывать вычисляемые свойства в зависимости от обновлений. Ваша задача — сделать функцию чистой и детерминированной: одинаковые входные данные — одинаковый результат, без побочных эффектов.
Заключение
Вычисляемые свойства (computed) во Vue.js — это удобный способ описывать производные значения, которые автоматически следят за своими зависимостями и кэшируются. Они помогают:
- отделять «данные» от их представления;
- избегать дублирования кода в шаблонах и методах;
- оптимизировать тяжелые вычисления за счет кэширования;
- делать код компонентов более чистым и предсказуемым.
Ключевые моменты, которые важно помнить:
- computed ведет себя как свойство, а не как функция;
- результат вычисляемого свойства кэшируется до изменения зависимостей;
- для побочных эффектов лучше использовать watch или методы;
- computed-функции должны быть «чистыми» — без асинхронных запросов и без мутации стороннего состояния;
- при необходимости можно использовать геттер и сеттер и даже двустороннее связывание через v-model.
Если вы будете воспринимать computed как декларативное описание того, «что можно получить из текущего состояния компонента», а не как «еще один способ написать функцию», то это станет мощным инструментом для упрощения логики и улучшения структуры кода.
Частозадаваемые технические вопросы по теме и ответы
Как сбросить кэш вычисляемого свойства вручную
Напрямую сбросить кэш нельзя. Vue сам инвалидирует кэш при изменении зависимостей. Если вам нужно явно пересчитать значение, сделайте его зависимым от дополнительного реактивного поля и меняйте это поле.
Пример:
data() {
return {
forceUpdateKey: 0,
value: 1
}
},
computed: {
computedValue() {
// Добавляем искусственную зависимость
void this.forceUpdateKey
return this.value * 2
}
},
methods: {
recalc() {
// Увеличиваем счетчик, чтобы заставить computed пересчитаться
this.forceUpdateKey++
}
}
Почему мое computed не обновляется при изменении объекта или массива
Часто проблема в том, что объект или массив мутируются не реактивно (в Vue 2) или вы не создаете новое значение. Решение:
- в Vue 2 используйте методы Vue.set или замену массива/объекта целиком;
- в Vue 3 старайтесь создавать новые объекты и массивы при изменении структуры.
// Вместо прямого присваивания свойства
this.obj.newProp = 1
// Используйте
this.obj = { ...this.obj, newProp: 1 }
Можно ли использовать computed внутри другого computed в Composition API
Да, можно. Computed в Composition API — это тоже реактивные источники. Внутри одного computed вы спокойно обращаетесь к другому:
const a = ref(1)
const doubled = computed(() => a.value * 2)
const tripledOfDoubled = computed(() => doubled.value * 3)
Vue сам выстроит цепочку зависимостей и пересчитает значения в нужном порядке.
Как типизировать computed в TypeScript с Composition API
Функция computed в Vue 3 умеет выводить тип автоматически, но при необходимости вы можете указать его явно:
const a = ref<number>(1)
// Здесь мы явно задаем тип для computed
const doubled = computed<number>(() => a.value * 2)
Для computed с get/set:
const fullName = computed<string>({
get() {
return firstName.value + ' ' + lastName.value
},
set(value: string) {
// ...
}
})
Почему computed с async/await возвращает Promise в шаблоне
Асинхронная функция всегда возвращает Promise. Computed не «ждет» этот Promise — он просто кэширует его как значение. В шаблоне вы увидите или [object Promise], или вообще ничего полезного. Решение — не использовать async в computed, а:
- сделать запрос в методе или хуке;
- сохранить результат в реактивное поле;
- использовать computed только для синхронных преобразований уже загруженных данных.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев