Сегодня посмотрим на вымышленный пример, как не надо делать стор. Любые совпадения — случайность. Все истории выдуманы.
Представьте: есть у нас герой Алекс. Перекидывают его на проект — «поправить пару простых багов, делов на пять минут». Открывает Алекс код, а там… У него сердце замирает. Подумаешь, с кем не бывает. Но внутри начинается дилема: просто пофиксить баги и забыть этот ужас как страшный сон, либо как настоящий богатырь проектов взять и отрефакторить весь этот бардак. Сделать по-человечески, заложить нормальную основу. Да, потом спросят за новые баги — ну и что. Зато внутри тепло разольётся, что не забил на плохой код и навёл порядок.

Ладно, хватит слов. Давайте к практике. Вот такой стор увидел Алекс (специально упростил, но суть та же):
export const usePropertyStore = defineStore('property', () => { const items = ref<Record<string, any>>({}) const isFetching = ref(false) watch( items, (val) => { localStorage.setItem('items', JSON.stringify(val)) }, { deep: true } ) const activeType = computed(() => localStorage.getItem('type')) const currentItem = computed(() => { if (!activeType.value) return null return items.value[activeType.value] }) async function load() { isFetching.value = true const result = await fetchProperties() isFetching.value = false return result } return { items, isFetching, activeType, currentItem, load }})
Алекса аж передёрнуло. Страшно было не то, что непонятно, а что этот код уже живой, его тестировали, им пользовались. Тронешь — и сам будешь отвечать за всё. Но Алекс не из робких. Собрался духом и начал разгребать.
Первое, что испугало — работа с localStorage прямо в сторе.

Сама по себе идея кешировать данные не криминал, но здесь ни одного хелпера, ни обработки ошибок. Если вдруг в localStorage что-то битое, JSON.parse выплюнет исключение и приложение упадёт в самый неподходящий момент. Я не говорю вообще не использовать localStorage, но в большинстве проектов он не нужен. А если и нужен — возьмите готовое решение от Pinia (плагин persistedstate), не изобретайте велосипедов. Если сами пишете обёртку — будьте добры, обрабатывайте парсинг и запись аккуратно, тестируйте. Здесь же просто запись без всякой защиты.
Что бы Алекс хотел увидеть:
export const saveToCache = (key, data) => { try { localStorage.setItem(key, JSON.stringify(data)) } catch (e) { // ... }}
Watch в сторе — почти всегда плохая практика.

Подняв историю коммитов и обсуждения в задачах, Алекс понял: это была незавершённая задумка — сохранять items в localStorage, чтобы после перезагрузки страницы сразу показывать данные, а не делать запрос заново. Логика понятна, но зачем городить watch с deep, который будет дёргаться на каждое изменение вложенных полей? Если бэк отдаёт быстро — лучше просто делать запрос по необходимости. Если уж очень надо кешировать — опять же, есть нормальные плагины. В нашем случае Алекс выяснил, что items можно было спокойно не хранить вообще, бэк давал данные за миллисекунды. Выпилили watch вместе с localStorage. Даже если необходимо сохранять, то есть понимание, где вызывается метод загрузки items, и мы можем сами отслеживать этот момент и вызывать localStorage.
Дальше полез в computed, который читает localStorage.getItem.
Честно, я сам офигел, когда Алекс показал. Вы когда-нибудь вешали чтение из localStorage прямо в computed? Мозг ломается, потому что это не работает.
activeType и currentItem выглядят реактивными, но это иллюзия. localStorage.getItem() — синхронная функция, которая не умеет сообщать Vue об изменениях. При первом рендере значение закешируется, а дальше, даже если пользователь обновит хранилище через консоль или другой компонент, computed не пересчитается.
Реактивность работает только с ref и reactive.
А как это использовалось? Нашлось быстро: в каком-то обработчике клика:
localStorage.setItem('selected_type', type)
И сразу же роутер пушили на новую страницу. Там уже стор заново обращался к этому computed и подхватывал значение. Жесть. Никогда так не делайте. Если нужно передать параметр между страницами — используйте роутер по-человечески: пропсы через route params, query. Это чище и предсказуемо.
Переменная isFetching в сторе резанула глаз.
Спрашиваю Алекса: «Зачем она здесь?» Он плечами пожал. Оказалось, isFetching во всём приложении больше никто не читал. Только сам метод load его менял — и всё. Ребята, запомните: не надо писать переменные в сторе про запас. Если вам нужно отслеживать состояние загрузки — создайте локальный ref в компоненте, где вызываете метод. Это и чище, и понятнее. Уносим loading из стора и больше не паримся.
Как это выглядит после правки
export const usePropertyStore = defineStore('property', () => { const items = ref<Record<string, any>>({}) const activeType = ref<string | null>(null) const currentItem = computed(() => activeType.value ? items.value[activeType.value] : null ) async function loadProperties() { return await fetchProperties() } return { photos, items, activeType, currentItem, loadProperties }})

Рефакторинг — это не про «сделать красиво». Это про «сделать предсказуемо». Когда стор не делает лишних движений, не кеширует то, что не должен, и не пытается управлять роутером, его легче тестировать, расширять и передавать по наследству. А если вам вдруг достался вот такой «кошмар» — не паникуйте. Разберите по шагам, уберите лишнее, замените на стандартные решения. И да, всегда гоняйте тесты перед мерджем. Удачного кодинга 🙂
ссылка на оригинал статьи https://habr.com/ru/articles/1028052/