Состояние приложения - как устроен state-management и зачем он нужен

05 января 2026
Автор

Олег Марков

Введение

Управление состоянием приложения (state-management) становится ключевой задачей, как только интерфейс перестает быть набором независимых страниц и превращается в живое, интерактивное приложение. Вы начинаете обрабатывать формы, запросы к серверу, уведомления, авторизацию, корзину, фильтры, меню. Каждый из этих элементов хранит данные, меняет их и как-то реагирует на изменения.

Состояние (state) — это не только данные с сервера. Это все, что влияет на то, как сейчас выглядит и работает интерфейс, начиная от выбранной вкладки и заканчивая списком товаров, подгруженных по API.

Если не продумать, как вы управляете этим состоянием, код быстро превращается в набор «заплаток»: события обрабатываются в разных местах, одно изменение ломает другое, а отладка превращается в поиск «кто в этот раз поменял эти данные».

В этой статье я разберу, что такое состояние приложения, какие его виды удобнее выделять, чем отличаются разные подходы к state-management и когда вообще стоит подключать «тяжелую артиллерию» вроде Redux или других библиотек. В примерах я буду опираться в первую очередь на фронтенд (SPA, React), но общие идеи подходят и для других платформ.

Что такое состояние приложения

Виды состояния

Чтобы не запутаться, полезно сначала разложить состояние на несколько категорий. Давайте сделаем это по простой и практичной схеме.

UI-состояние (view state)

Это состояние, которое описывает, как сейчас выглядит интерфейс:

  • открыты ли модальные окна
  • какая вкладка выбрана
  • какой элемент выделен
  • какой фильтр включен в данный момент
  • состояние пагинации (активная страница)

Пример (на псевдокоде в стиле React):

// Состояние, управляющее только внешним видом интерфейса
const [isModalOpen, setIsModalOpen] = useState(false) // Открыт ли модал
const [activeTab, setActiveTab] = useState('info')    // Какая вкладка выбрана

Это состояние обычно «живёт» рядом с компонентом, который его использует, и никуда далеко не распространяется.

Доменное (бизнес-)состояние

Это «смысловые» данные, с которыми работает бизнес-логика:

  • текущий пользователь и его права
  • список товаров
  • корзина
  • список заказов
  • баланс на счете

Именно доменное состояние чаще всего нужно показывать в разных местах приложения и хранить дольше, чем одно посещение страницы.

Пример:

// Данные, которые приходят по API и имеют бизнес-смысл
const [user, setUser] = useState(null)          // Авторизованный пользователь
const [cartItems, setCartItems] = useState([])  // Товары в корзине

Состояние процесса (async / network state)

Это все, что связано с текущими операциями:

  • идет ли загрузка данных
  • были ли ошибки
  • в очереди ли запрос
  • успешно ли завершилась операция

Пример:

// Состояние, описывающее процесс взаимодействия с API
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)

Здесь важно не путать «результат» (например, список пользователей) и «процесс» (идет ли загрузка этого списка).

Кэш и серверное состояние

Отдельно часто выделяют серверное состояние (server state):

  • данные, которые приходят с бэкенда
  • данные, которые можно повторно запросить
  • данные, которые могут быть устаревшими и их нужно обновлять

Особенность: такое состояние обычно не нужно хранить «навсегда», его проще заново загрузить с сервера, чем пытаться поддерживать синхронность между клиентом и сервером. Именно для этого появились библиотеки типа React Query, SWR и им подобные.

Почему важно разделять состояние

Если не разделять виды состояния, можно столкнуться с типичными проблемами:

  • UI-состояние и доменные данные смешаны в одном месте
  • непонятно, что можно просто перезагрузить с сервера, а что хранится только на клиенте
  • сложнее тестировать бизнес-логику: она растворена внутри компонентов

Когда вы разделяете:

  • UI-состояние живет ближе к компонентам
  • доменное состояние можно вынести в отдельный слой (store, сервисы)
  • серверное состояние можно доверить специальным библиотекам

Теперь давайте разберем основные подходы к управлению состоянием.

Локальное состояние в компонентах

Когда локального состояния достаточно

Локальное состояние — это переменные, которые «живут» внутри конкретного компонента или отдельного модуля. В React это useState, useReducer, в Vue — data / ref / reactive, в Svelte — обычные переменные компонента.

Локальное состояние отлично подходит, когда:

  • данные используются только в одном или паре ближайших компонентов
  • состояние описывает исключительно внешний вид (UI-состояние)
  • нет сложных зависимостей между этими данными и другим кодом

Пример локального состояния на React

Смотрите, я покажу вам простой пример управления состоянием для формы логина.

import { useState } from 'react'

function LoginForm() {
  // Локальное состояние полей формы
  const [email, setEmail] = useState('')       // Значение поля email
  const [password, setPassword] = useState('') // Значение поля password
  
  // Локальное состояние процесса
  const [isSubmitting, setIsSubmitting] = useState(false) // Идет ли отправка
  const [error, setError] = useState(null)                // Ошибка при отправке

  const handleSubmit = async (event) => {
    event.preventDefault() // Отменяем стандартное поведение формы
    setIsSubmitting(true)  // Устанавливаем флаг "отправка началась"
    setError(null)         // Сбрасываем прошлую ошибку

    try {
      // Здесь мы могли бы отправить запрос на сервер
      await fakeLogin(email, password) // fakeLogin - условная функция логина
    } catch (e) {
      setError('Ошибка авторизации') // Сохраняем текст ошибки в состоянии
    } finally {
      setIsSubmitting(false) // Сбрасываем флаг "отправка идет"
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* Поле email управляется через локальное состояние */}
      <input
        value={email}
        onChange={e => setEmail(e.target.value)} // Обновляем состояние при вводе
        placeholder="Email"
      />
      {/* Поле password также использует локальное состояние */}
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)} // Обновляем при вводе
        placeholder="Пароль"
      />
      {/* Сообщение об ошибке зависит от состояния error */}
      {error && <div style={{ color: 'red' }}>{error}</div>}
      {/* Кнопка блокируется, когда идет отправка */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Входим...' : 'Войти'}
      </button>
    </form>
  )
}

// Условная функция, имитирующая логин
async function fakeLogin(email, password) {
  // Здесь можно представить запрос к серверу
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // В демонстрационных целях всегда делаем "успех"
      resolve()
    }, 1000)
  })
}

Как видите, все состояние здесь локально и не нужно никуда его «поднимать». Это самый простой и понятный вариант.

Проблемы локального состояния

Чем больше вы строите приложение на одном только локальном состоянии, тем чаще появляются ситуации, когда:

  • те же данные нужны в нескольких несвязанных компонентах (например, профиль пользователя — в шапке и на странице аккаунта)
  • при изменении данных нужно обновить много разных частей интерфейса
  • для изменения одного значения приходится пробрасывать callbacks через несколько уровней компонентов

Вот здесь и появляется вопрос о глобальном состоянии.

Глобальное состояние и «подъем состояния»

Подъем состояния (lifting state up)

Подъем состояния — это когда вы выносите общие данные выше по дереву компонентов, чтобы несколько дочерних компонентов могли их использовать.

Давайте разберемся на примере: есть два компонента — список фильтров и список товаров. Фильтры меняют состояние, а товары должны на него реагировать.

function ProductsPage() {
  // Состояние фильтров на уровне родителя
  const [filters, setFilters] = useState({
    search: '',   // Строка поиска
    category: ''  // Выбранная категория
  })

  return (
    <div>
      {/* Передаем состояние и функцию обновления в Filters */}
      <Filters filters={filters} onChange={setFilters} />
      {/* В Products передаем только нужные значения */}
      <Products filters={filters} />
    </div>
  )
}

function Filters({ filters, onChange }) {
  // Обработчик изменения строки поиска
  const handleSearchChange = (event) => {
    const value = event.target.value
    // Обновляем общий объект фильтров
    onChange({ ...filters, search: value })
  }

  return (
    <input
      placeholder="Поиск"
      value={filters.search}
      onChange={handleSearchChange}
    />
  )
}

function Products({ filters }) {
  // Здесь могли бы фильтровать товары по filters.search и filters.category
  // Пример условного использования
  // const filteredProducts = products.filter(p => p.name.includes(filters.search))
  return (
    <div>
      {/* Здесь показываем список товаров с учетом фильтров */}
      Список товаров c фильтром: {filters.search}
    </div>
  )
}

Такой подход хорошо работает, пока:

  • уровней вложенности немного
  • состояние используют в основном дочерние компоненты

Как только вы начинаете тянуть состояние «через несколько уровней вниз» к компонентам, которые его используют, компоненты посредники начинают «надуваться» и теряется читаемость.

Контекст как простой глобальный стор (на примере React)

Когда подъем состояния усложняется, часто переходят к механизму контекста (Context API). Он позволяет объявить «глобальное» для поддерева состояние и использовать его без явной передачи через пропсы.

Покажу вам, как это выглядит.

import { createContext, useContext, useState } from 'react'

// Создаем контекст для авторизации
const AuthContext = createContext(null)

export function AuthProvider({ children }) {
  // Состояние текущего пользователя
  const [user, setUser] = useState(null)

  // Функция логина
  const login = (userData) => {
    setUser(userData) // Сохраняем данные пользователя
  }

  // Функция логаута
  const logout = () => {
    setUser(null) // Очищаем состояние пользователя
  }

  // Объект, который будет доступен в любых дочерних компонентах
  const value = { user, login, logout }

  return (
    // Оборачиваем все приложение в провайдер
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  )
}

// Хук для удобного использования контекста
export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth должен использоваться внутри AuthProvider')
  }
  return context
}

Теперь вы можете использовать состояние авторизации в любом месте приложения:

function Header() {
  const { user, logout } = useAuth() // Получаем состояние и функции из контекста

  return (
    <header>
      {user ? (
        <>
          <span>Привет, {user.name}</span>
          <button onClick={logout}>Выйти</button>
        </>
      ) : (
        <span>Гость</span>
      )}
    </header>
  )
}

Контекст — это уже простейший state-management. Но у него есть ограничения:

  • сложно контролировать, какие компоненты будут перерисовываться
  • логика может разрастаться внутри провайдеров
  • для сложных сценариев начинают дублироваться решения, которые уже реализованы в специализированных библиотеках

Паттерн централизованного хранилища (store)

Идея «единого источника правды»

Когда приложение становится достаточно сложным, появляется идея: выделить слой, который будет отвечать за состояние приложения и его изменения. Такой слой часто называют store (хранилище).

Базовые принципы централизованного хранилища:

  • у вас есть единый источник правды — объект или набор объектов, в которых лежит текущее состояние
  • изменение состояния происходит только через определенные функции (actions, reducers, методы)
  • интерфейс «подписывается» на изменения и обновляется, когда нужные части состояния изменяются

Это не привязано к конкретной библиотеке. Подобный подход можно реализовать самостоятельно и на чистом JavaScript.

Давайте посмотрим простую, «ручную» реализацию.

Пример простого store на чистом JavaScript

Здесь я размещаю пример, чтобы вам было проще понять, как работает подписка на изменения.

// Простое хранилище состояния
function createStore(initialState) {
  let state = initialState            // Текущее состояние
  const listeners = new Set()         // Набор подписчиков

  // Функция получения состояния
  const getState = () => state

  // Функция подписки на изменения
  const subscribe = (listener) => {
    listeners.add(listener)           // Добавляем слушателя
    return () => listeners.delete(listener) // Возвращаем функцию отписки
  }

  // Функция обновления состояния
  const setState = (updater) => {
    // updater может быть объектом или функцией
    const nextState =
      typeof updater === 'function' ? updater(state) : updater

    // Обновляем ссылку на состояние
    state = { ...state, ...nextState }

    // Уведомляем всех подписчиков
    listeners.forEach((listener) => listener(state))
  }

  return { getState, setState, subscribe }
}

// Использование
const store = createStore({ count: 0 }) // Задаем начальное состояние

// Подписываемся на изменения
const unsubscribe = store.subscribe((state) => {
  console.log('Новое состояние', state)
})

// Меняем состояние
store.setState({ count: 1 }) // Лог: Новое состояние { count: 1 }
store.setState((prev) => ({ count: prev.count + 1 })) // Лог: { count: 2 }

// Отписываемся, когда больше не нужно следить
unsubscribe()

Этот подход уже дает вам:

  • единый объект состояния
  • контролируемые способы обновления
  • подписку на изменения

Библиотеки вроде Redux, Zustand, MobX, Pinia, Vuex развивают эти идеи дальше: добавляют удобный синтаксис, интеграцию с фреймворками, инструменты отладки.

Иммутабельность состояния

Почему важна иммутабельность

Во многих подходах к state-management используется принцип иммутабельности: вы не меняете существующий объект состояния «на месте», а создаете новый объект на основе старого.

Это нужно для:

  • предсказуемости: вы знаете, когда именно состояние меняется
  • простоты отката изменений (time-travel, undo/redo)
  • оптимизации производительности через сравнение ссылок (shallow compare)
  • упрощения отладки, так как каждое изменение — это новый объект

Пример «плохого» и «хорошего» обновления массива:

// Плохо - мутируем существующий массив
const state = { items: [1, 2, 3] }
state.items.push(4) // Изменяем существующий массив

// Хорошо - создаем новый массив
const state2 = { items: [1, 2, 3] }
const nextState = {
  ...state2,                      // Копируем старое состояние
  items: [...state2.items, 4]     // Создаем новый массив с добавленным элементом
}

Иммутабельность — один из ключевых принципов в Redux и похожих библиотеках.

Redux как пример детерминированного state-management

Основные концепции Redux

Redux — это популярная библиотека для управления состоянием в JavaScript-приложениях, основанная на нескольких идеях:

  • одно глобальное хранилище (store) для всего состояния
  • состояние только для чтения (read-only)
  • единственный способ изменить состояние — диспатчинг действий (actions)
  • чистые функции reducers описывают, как состояние меняется при разных действиях

Теперь вы увидите, как это выглядит в коде.

Структура Redux

  1. Store — объект, который хранит состояние.
  2. Action — обычный объект, который описывает, «что произошло».
  3. Reducer — чистая функция (state, action) => newState.

Пример Redux-слайса с использованием Redux Toolkit

Сейчас для работы с Redux чаще всего используют Redux Toolkit (RTK), который снижает «боулерплейт» и упрощает работу с иммутабельностью.

import { configureStore, createSlice } from '@reduxjs/toolkit'

// Создаем слайс состояния для счетчика
const counterSlice = createSlice({
  name: 'counter',        // Имя слайса
  initialState: { value: 0 }, // Начальное состояние
  reducers: {
    increment(state) {
      // Здесь мы как будто мутируем state
      // Но под капотом RTK использует Immer и делает иммутабельное обновление
      state.value += 1
    },
    decrement(state) {
      state.value -= 1
    },
    addByAmount(state, action) {
      // action.payload - значение, переданное при вызове экшена
      state.value += action.payload
    }
  }
})

// Получаем редьюсер и экшены
export const { increment, decrement, addByAmount } = counterSlice.actions

// Создаем store
export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer // Регистрируем редьюсер в корневом сторе
  }
})

Использование этого состояния в React-компонентах через react-redux:

import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, addByAmount } from './counterSlice'

function Counter() {
  // Берем значение из состояния
  const value = useSelector(state => state.counter.value) // Достаем счетчик
  // Получаем функцию dispatch
  const dispatch = useDispatch()

  return (
    <div>
      <div>Значение: {value}</div>
      {/* Вызываем экшены через dispatch */}
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(addByAmount(5))}>+5</button>
    </div>
  )
}

Обратите внимание, как этот фрагмент кода решает задачу:

  • компонент не знает деталей внутренней реализации хранилища
  • вся логика изменения состояния находится в слайсе
  • легко тестировать редьюсеры отдельно от компонентов

Когда имеет смысл использовать Redux

Redux оправдан, когда:

  • у вас большое приложение с множеством независимых модулей
  • есть сложные сценарии обновления данных (например, несколько разных операций над одними и теми же сущностями)
  • важно иметь детализированную историю изменений состояния (для отладки, аудита)
  • нужно предсказуемое поведение и строгие правила изменения состояния

Если у вас маленькое приложение или простая форма, Redux может быть избыточен.

Альтернативные библиотеки и подходы

Zustand — минималистичный store

Zustand предлагает простой API, который ближе к примеру с createStore, который мы уже рассматривали. Часто выбирают, когда нужно небольшое, но гибкое глобальное хранилище.

Пример Zustand-store:

import create from 'zustand'

// Создаем хук useStore
export const useStore = create((set, get) => ({
  count: 0,    // Начальное состояние
  // Метод увеличения
  increment: () => set(state => ({ count: state.count + 1 })),
  // Метод уменьшения
  decrement: () => set(state => ({ count: state.count - 1 })),
  // Метод, использующий текущее состояние через get
  incrementIfOdd: () => {
    const { count } = get()  // Получаем текущее значение
    if (count % 2 === 1) {
      set({ count: count + 1 })
    }
  }
}))

Использование в компоненте:

function Counter() {
  // Деструктурируем нужные части состояния и методы
  const { count, increment, decrement } = useStore()

  return (
    <div>
      <div>Значение: {count}</div>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  )
}

Zustand не навязывает иммутабельность, структуру и паттерн «actions/reducers», но при этом дает:

  • простую подписку на определенные части состояния
  • легкую интеграцию с React через хуки

MobX — реактивный подход

MobX строится вокруг идеи «обсервабельных» данных (observable) и реактивных вычислений. Вы описываете:

  • какие данные «наблюдаемые»
  • какие вычисления зависят от этих данных

MobX самостоятельно отслеживает зависимости и обновляет только нужные части интерфейса.

Плюсы:

  • меньше кода для сложных связей между данными
  • естественная интеграция с ООП-стилем (классы, методы)

Минус — сложнее строго контролировать поток данных, чем в Redux.

React Query / SWR — управление серверным состоянием

Отдельная категория инструментов — библиотеки, которые берут на себя работу с серверным состоянием: загрузка, кэширование, инвалидация, повторные запросы, обработка ошибок.

React Query пример:

import { useQuery } from '@tanstack/react-query'
import axios from 'axios'

function UsersList() {
  // useQuery сам управляет isLoading, error и данными
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],           // Ключ кэша
    queryFn: () => axios.get('/api/users').then(res => res.data) // Функция запроса
  })

  if (isLoading) return <div>Загрузка...</div>
  if (error) return <div>Ошибка</div>

  return (
    <ul>
      {/* data - это массив пользователей */}
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Чем хорош такой подход:

  • вы не храните серверные данные руками в Redux или другом сторе
  • библиотека решает задачи повторной загрузки, фонового обновления и т.п.
  • сосредотачиваетесь на бизнес-логике, а не на кэширующей инфраструктуре

Как выбирать подход к state-management

Несколько практичных правил

Давайте посмотрим, как можно подойти к выбору инструмента и архитектуры.

1. Начинайте с локального состояния

Если вы пишете новый модуль или страницу:

  • сначала используйте локальное состояние (useState, useReducer, data, ref)
  • разделяйте UI-состояние, доменное состояние и состояние процессов
  • не вводите глобальные концепции, пока это не станет реальной проблемой

2. Поднимайте состояние по мере необходимости

Когда вы понимаете, что:

  • данные нужны нескольким компонентам
  • связи между компонентами становятся плотнее

— поднимайте состояние выше по дереву, создавайте кастомные хуки, используйте контекст.

3. Выделяйте доменный слой

Если бизнес-логика становится сложной:

  • выделяйте доменные сущности (пользователь, заказ, товар)
  • создавайте сервисы или отдельные модули для работы с ними
  • не смешивайте детали API, формат данных и UI-логику в компонентах

Это можно сделать и без Redux — просто вынести логику в отдельные модули.

4. Подключайте специализированные библиотеки при росте сложности

Подумайте о Redux, Zustand, MobX и других инструментах, если:

  • состояние нужно переиспользовать во многих частях приложения
  • появляется сложная синхронизация клиент–сервер
  • нужно четко контролировать поток данных, логировать действия, строить отчеты

Для серверного состояния чаще выгоднее использовать React Query / SWR, чем тащить все в один «глобальный стор».

Типичные ошибки при state-management

Вот несколько ситуаций, которые часто приводят к проблемам:

  • попытка все состояние держать в одном огромном глобальном сторе
  • смешение серверного состояния и состояния UI в одном хранилище
  • отсутствие правил, кто и как может изменять состояние
  • хранение в состоянии того, что можно вычислить «на лету»

Пример лишнего состояния:

// Плохо - мы храним derived state, которое легко вычисляется
const [fullName, setFullName] = useState('')

const firstName = 'Иван'
const lastName = 'Иванов'

// fullName можно просто вычислить как `${firstName} ${lastName}`

Лучше хранить только то, что нельзя однозначно вычислить по другим данным.

Теперь давайте перейдем к краткому резюме.


Управление состоянием приложения — это, по сути, управление сложностью. Чем больше взаимосвязей между данными и интерфейсом, тем важнее становится четкая архитектура state-management.

Ключевые идеи, которые стоит удерживать:

  • состояние — это все, что влияет на текущее отображение интерфейса, а не только данные с сервера
  • полезно разграничивать UI-состояние, доменное состояние и состояние процессов
  • начинайте с простого: локальное состояние, подъем, контекст
  • не смешивайте разные типы состояния без необходимости
  • используйте специализированные библиотеки, когда размер и сложность приложения этого требуют
  • старайтесь держать бизнес-логику рядом с доменными сущностями, а не внутри компонентов

Если вы будете постепенно усложнять архитектуру по мере необходимости, а не «на всякий случай», вы сохраните код понятным и управляемым.

Частозадаваемые технические вопросы по теме и ответы

Как организовать откат состояния (undo/redo) в приложении

Обычно используют один из двух подходов:

  1. История состояний:

    • храните массив previousStates
    • при каждом изменении состояния добавляйте в историю предыдущую версию
    • для undo берите последнее состояние из истории и делайте его текущим
    • для redo храните отдельный стек «вперед»
  2. История действий:

    • храните список actions, которые привели к текущему состоянию
    • храните также inverseActions, которые отменяют действие
    • при undo выполняйте inverseAction для последнего action

Практически проще начать с хранения предыдущих состояний, но следите за объемом памяти (обрезайте историю по длине).

Как лучше хранить сущности - в виде массива или словаря по id

Для коллекций, к которым часто обращаются по id, удобно использовать нормализованный вид:

  • entitiesById — объект, где ключи это id, а значения это сущности
  • allIds — массив всех id (для сохранения порядка)

Пример структуры:

{
  usersById: {
    '1': { id: '1', name: 'Аня' },
    '2': { id: '2', name: 'Петя' }
  },
  allUserIds: ['1', '2']
}

Так проще обновлять, удалять и искать сущности.

Как синхронизировать состояние между вкладками браузера

Используйте один из вариантов:

  • localStorage + событие storage:

    • при изменении состояния записывайте его (или нужные части) в localStorage
    • в других вкладках подписывайтесь на событие window.addEventListener('storage', handler)
    • при изменении обновляйте локальное состояние
  • BroadcastChannel:

    • создайте канал new BroadcastChannel('app')
    • отправляйте сообщения о важных изменениях
    • во всех вкладках подписывайтесь на канал и обновляйте состояние

Не отправляйте через эти механизмы «все состояние целиком» без необходимости — выбирайте только нужные части.

Как избежать лишних перерисовок при глобальном состоянии

Основные приемы:

  • делите состояние на независимые слайсы (user, cart, ui)
  • в React используйте селекторы, которые выбирают только нужные части состояния
  • мемоизируйте вычисляемые данные (reselect в Redux, useMemo в React)
  • в контексте разбивайте контексты по ответственности, чтобы одно изменение не перерисовывало всю поддеревню

Главная идея — чем меньше данных компонент читает из глобального хранилища, тем реже он будет перерисовываться.

Как тестировать логику изменения состояния отдельно от UI

Вынесите бизнес-логику в чистые функции или редьюсеры:

  • функция принимает текущее состояние и действие
  • возвращает новое состояние
  • не использует внешние эффекты (запросы, таймеры и т.д.)

Дальше пишите unit-тесты:

  • задаете начальное состояние
  • вызываете функцию с нужным действием
  • проверяете, что вернулось ожидаемое состояние

Так вы сможете менять UI, не трогая тесты логики, и наоборот.

Стрелочка влевоСтилизация в FSD stylingМокирование данных mocking в тестированииСтрелочка вправо

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий