Олег Марков
Локальное состояние local state в веб разработке
Введение
Локальное состояние (local state) — это данные, которые живут внутри конкретного компонента, функции или модуля и нужны только ему или небольшой группе связанных элементов интерфейса. В отличие от глобального состояния, локальное не предназначено для распространения по всему приложению и обычно существует ровно столько, сколько живет соответствующий компонент.
Если говорить простыми словами, локальное состояние — это внутренние переменные вашего интерфейса, которые определяют его текущее поведение и внешний вид. Например:
- открыт ли модальный диалог
- какой таб выбран сейчас
- введен ли текст в поле формы
- какое значение у локального фильтра списка
Давайте разберем, зачем вообще разделять состояние на локальное и глобальное, как именно работать с локальным состоянием в разных сценариях и как не превратить код в хаос по мере роста проекта.
Что такое локальное состояние и чем оно отличается от глобального
Основные характеристики локального состояния
Локальное состояние обладает несколькими важными свойствами.
Область видимости ограничена
- переменные состояния доступны только внутри конкретного компонента или модуля
- другой код не может напрямую изменить это состояние, пока вы сами его не передадите
Время жизни привязано к компоненту
- состояние создается при инициализации компонента
- очищается, когда компонент удаляется или размонтируется
Назначение — управление поведением конкретной части UI
- локальный флаг
isOpenрешает, показывать или скрывать конкретное окно - локальное поле
searchQueryиспользуется только в одном компоненте поиска
- локальный флаг
Простота изоляции
- локальное состояние легко тестировать и рефакторить
- меньше риск случайно затронуть другие части приложения
Различия между локальным и глобальным состоянием
Глобальное состояние (например, в Redux, Zustand, Vuex, Pinia и подобных инструментах) предназначено для данных, которые:
- нужны многим частям приложения
- должны переживать смену экранов или маршрутов
- важны для бизнес логики и доменной модели (пользователь, корзина, настройки)
Локальное состояние лучше использовать, когда:
- данные отвечают только за UI, а не за бизнес данные
- никто, кроме конкретного компонента, не должен знать об этих данных
- состояние не важно после того, как пользователь покидает этот экран
Смотрите, если вы можете безболезненно удалить компонент вместе с его данными — почти наверняка это локальное состояние.
Где локальное состояние используется чаще всего
Типичные примеры локального состояния
Пройдемся по привычным сценариям:
состояние инпутов и форм
- текст в полях
- чекбоксы и радиокнопки
- временные ошибки валидации
визуальные флаги
- открыт ли дропдаун
- активна ли подсказка
- выбран ли элемент списка
временные вычисления
- локальная сортировка
- фильтрация видимого списка
- выбранная страница пагинации
технические состояния
isLoadingтолько для одной кнопкиisSubmittingтолько для этой формыhasCopiedдля локального копирования в буфер
Давайте посмотрим, как это выглядит в живом коде на примере React. Здесь просто удобно показать ideas, но сами принципы справедливы и для других фреймворков.
// Простой компонент поиска с локальным состоянием
import { useState } from "react"
function SearchBox() {
// Здесь мы создаем локальное состояние searchQuery
// Оно нужно только этому компоненту
const [searchQuery, setSearchQuery] = useState("")
// Обработчик изменения текста в инпуте
const handleChange = (event) => {
// Обновляем локальное состояние при каждом вводе
setSearchQuery(event.target.value)
}
return (
<div>
{/* Поле ввода, привязанное к локальному состоянию */}
<input
type="text"
value={searchQuery} // Здесь используем текущее значение
onChange={handleChange} // Здесь обновляем значение
placeholder="Введите запрос"
/>
{/* Отображаем текущее состояние рядом, для примера */}
<p>Текущий запрос - {searchQuery}</p>
</div>
)
}
Как видите, это состояние не нужно никому за пределами SearchBox, ему место в локальном хранилище самого компонента.
Основные принципы работы с локальным состоянием
Принцип 1: Минимально необходимое состояние
Старайтесь хранить в состоянии только то, что нельзя легко вычислить из других данных. Избыточное локальное состояние:
- усложняет код
- повышает риск несоответствий
- требует дополнительных обработчиков
Пример. Плохо, когда вы храните одновременно:
- исходные данные списка
- отфильтрованный список
- количество элементов после фильтра
Часть этих данных можно вычислять:
// Псевдокод без привязки к конкретному фреймворку
// Лучше хранить только исходные данные и критерий фильтрации
const state = {
items: [/* ... */], // Здесь исходные элементы
filter: "active", // Здесь критерий фильтрации
}
// А отфильтрованный список и количество считать при необходимости
const visibleItems = items.filter(item => item.status === filter)
// visibleItems.length можно получить в любой момент без отдельного состояния
Принцип 2: Локальное состояние должно быть рядом с используемым кодом
Чем ближе объявление состояния к месту использования, тем проще сопровождать код. Если состояние используется только в одном компоненте, не нужно выносить его выше по иерархии.
Если вы начинаете поднимать состояние все выше и выше только ради передачи пары значений — это сигнал, что вам, возможно, нужен контекст, глобальное состояние или другой паттерн. Но до этого момента держите состояние локально.
Принцип 3: Четкая ответственность
Каждое локальное состояние должно отвечать за конкретный аспект поведения. Если вы не можете объяснить, за что отвечает переменная, или название слишком абстрактное (data, info, value), попробуйте его переосмыслить.
Локальное состояние в компонентном подходе
Пример в React useState
Давайте разберем базовый паттерн с useState чуть подробнее.
import { useState } from "react"
function Counter() {
// Здесь мы создаем локальное состояние count со стартовым значением 0
const [count, setCount] = useState(0)
// Функция увеличения счетчика
const increment = () => {
// Обновляем состояние на основе предыдущего значения
setCount(prevCount => prevCount + 1)
}
// Функция сброса счетчика
const reset = () => {
// Возвращаемся к начальному значению
setCount(0)
}
return (
<div>
<p>Текущее значение - {count}</p>
<button onClick={increment}>
Увеличить
</button>
<button onClick={reset}>
Сбросить
</button>
</div>
)
}
Здесь локальное состояние:
- живет внутри
Counter - создается при первом рендере компонента
- очищается, когда
Counterисчезает из интерфейса
Ни один другой компонент не знает про count напрямую.
Пример в Vue ref и data
Похожая идея в Vue (Composition API):
// Пример компонента на Vue 3 с локальным состоянием
import { ref } from "vue"
export default {
setup() {
// Создаем локальное состояние isOpen для управления модальным окном
const isOpen = ref(false)
// Функция для открытия модального окна
const open = () => {
isOpen.value = true
}
// Функция для закрытия модального окна
const close = () => {
isOpen.value = false
}
// Возвращаем локальное состояние и методы в шаблон
return { isOpen, open, close }
}
}
Локальное состояние здесь — isOpen. Оно не доступно извне, пока вы явно не начнете его пробрасывать вверх или вниз.
Управление формами с помощью локального состояния
Формы — один из самых насыщенных локальным состоянием элементов UI. Смотрите, разберем реальный пример формы логина.
// Форма авторизации с локальным состоянием для полей и ошибок
import { useState } from "react"
function LoginForm() {
// Локальные состояния для значений полей
const [email, setEmail] = useState("") // Здесь хранится email пользователя
const [password, setPassword] = useState("") // Здесь хранится пароль
// Локальное состояние для ошибок формы
const [error, setError] = useState(null) // Здесь будет текст ошибки или null
// Локальное состояние для индикатора отправки
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async (event) => {
event.preventDefault() // Отменяем стандартную отправку формы
setError(null) // Сбрасываем прошлую ошибку
setIsSubmitting(true) // Отмечаем, что идет отправка
try {
// Здесь мог бы быть вызов API для авторизации
// await api.login({ email, password })
// Для примера эмулируем успешный ответ небольшой задержкой
await new Promise(resolve => setTimeout(resolve, 500))
// После успешной авторизации можно очистить форму
setPassword("") // Обычно пароль очищают
// Email иногда оставляют, здесь можно решить по ситуации
} catch (e) {
// В случае ошибки записываем сообщение в локальное состояние error
setError("Не удалось войти. Проверьте email и пароль")
} finally {
// В любом случае снимаем флаг отправки
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Email
<input
type="email"
value={email} // Привязываем к локальному состоянию
onChange={(e) => setEmail(e.target.value)} // Обновляем email
/>
</label>
</div>
<div>
<label>
Пароль
<input
type="password"
value={password} // Привязываем к локальному состоянию
onChange={(e) => setPassword(e.target.value)} // Обновляем пароль
/>
</label>
</div>
{/* Если есть ошибка, показываем ее пользователю */}
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Входим..." : "Войти"}
</button>
</form>
)
}
Здесь вы видите целый набор локальных состояний, каждое отвечает за свою задачу:
email,password— значения полейerror— сообщение об ошибкеisSubmitting— технический индикатор отправки формы
Все это локально, потому что:
- никому, кроме формы, не нужно знать процесс ввода
- глобальному хранилищу важен только итог — успешный вход и данные пользователя
- состояния формы теряют смысл при уходе со страницы
Локальное состояние и побочные эффекты
Почему важно разделять состояние и эффекты
Локальное состояние отвечает за данные, а эффекты — за взаимодействие с внешним миром:
- запросы к API
- подписки
- таймеры
- работа с DOM напрямую
Хорошая практика — разделять:
- что мы храним (состояние)
- что мы делаем в ответ на изменения (эффекты)
Пример (на React с useEffect), где мы подгружаем данные и управляем локальным состоянием загрузки:
import { useState, useEffect } from "react"
function UsersList() {
// Локальное состояние для списка пользователей
const [users, setUsers] = useState([])
// Локальное состояние для признака загрузки
const [isLoading, setIsLoading] = useState(false)
// Локальное состояние для ошибки
const [error, setError] = useState(null)
useEffect(() => {
// Здесь мы запускаем побочный эффект при монтировании компонента
const loadUsers = async () => {
setIsLoading(true) // Отмечаем, что началась загрузка
setError(null) // Сбрасываем прошлую ошибку
try {
// Запрашиваем данные с сервера
const response = await fetch("/api/users")
if (!response.ok) {
throw new Error("Ошибка загрузки списка пользователей")
}
const data = await response.json()
setUsers(data) // Сохраняем полученные данные в локальное состояние
} catch (e) {
// При ошибке сохраняем текст ошибки в состояние
setError(e.message)
} finally {
// В любом случае снимаем флаг загрузки
setIsLoading(false)
}
}
loadUsers()
}, []) // Пустой массив означает запуск только при первом рендере
if (isLoading) {
return <p>Загрузка...</p> // Показываем индикатор загрузки
}
if (error) {
return <p style={{ color: "red" }}>Ошибка - {error}</p> // Показываем ошибку
}
return (
<ul>
{/* Отображаем список пользователей из локального состояния */}
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
Локальное состояние здесь играет роль связующего звена между запросом и интерфейсом, но при этом оно не превращается в глобальный источник правды для всего приложения.
Когда локального состояния уже недостаточно
Типичные сигналы, что пора поднимать состояние выше
Иногда вы начинаете с локального состояния, но затем понимаете, что оно нужно нескольким компонентам. Вот признаки:
- одно и то же состояние приходится пробрасывать через 3–4 уровня пропсов
- состояние нужно и родителю, и нескольким дочерним компонентам
- разные части интерфейса должны синхронно реагировать на изменения этих данных
Пример: у вас есть компонент фильтров и компонент списка товаров. Логично сделать фильтры локальными, но:
- список должен знать про активные фильтры
- возможно, заголовок страницы должен отображать количество найденных товаров
В такой ситуации состояние фильтров стоит поднять до ближайшего общего предка этих компонентов, а сами фильтры и список будут получать данные и функции изменения через параметры или контекст.
Переход от локального состояния к глобальному
Вы можете начать с локального состояния и со временем:
- Поднять его в родительский компонент
- Потом использовать контекст
- Потом вынести в глобальное хранилище (если действительно нужно)
Важно, что путь не обратный: начинать с глобального состояния и потом локализовывать сложнее, чем наоборот.
Структурирование сложного локального состояния
Когда локального состояния становится много, важно его структурировать, а не просто добавлять новые переменные.
Подход 1: Группировка в объект
Если несколько полей логически связаны (например, поля формы), их удобно хранить в одном объекте.
import { useState } from "react"
function ProfileForm() {
// Здесь мы создаем локальное состояние для всей формы сразу
const [form, setForm] = useState({
firstName: "", // Имя пользователя
lastName: "", // Фамилия
age: "", // Возраст
})
const handleChange = (field) => (event) => {
const value = event.target.value
// Обновляем только одно поле, сохраняя остальные
setForm(prevForm => ({
...prevForm, // Копируем предыдущие значения
[field]: value, // Заменяем нужное поле
}))
}
const handleSubmit = (event) => {
event.preventDefault()
// Здесь можно отправить form на сервер
// console.log(form)
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Имя"
value={form.firstName}
onChange={handleChange("firstName")} // Обновляем только firstName
/>
<input
type="text"
placeholder="Фамилия"
value={form.lastName}
onChange={handleChange("lastName")} // Обновляем только lastName
/>
<input
type="number"
placeholder="Возраст"
value={form.age}
onChange={handleChange("age")} // Обновляем только age
/>
<button type="submit">Сохранить</button>
</form>
)
}
Такой подход упрощает:
- передачу состояния в другие функции
- сброс всей формы одним действием
- валидацию нескольких полей одновременно
Подход 2: useReducer или аналогичные механизмы
Если логика обновления состояния становится сложнее, чем простое присвоение, полезно выделять ее в отдельный редьюсер.
import { useReducer } from "react"
// Редьюсер описывает, как изменяется состояние в ответ на действия
function formReducer(state, action) {
switch (action.type) {
case "CHANGE_FIELD":
// Обновляем конкретное поле формы
return {
...state,
[action.field]: action.value,
}
case "RESET":
// Сбрасываем форму к начальному значению
return action.initialState
default:
return state
}
}
function SettingsForm() {
const initialState = {
theme: "light", // Тема оформления
notifications: true // Включены ли уведомления
}
// Здесь мы создаем локальное состояние через useReducer
const [state, dispatch] = useReducer(formReducer, initialState)
const handleChange = (field) => (event) => {
const value =
event.target.type === "checkbox"
? event.target.checked // Для чекбокса берем checked
: event.target.value // Для остальных полей берем value
// Отправляем действие для редьюсера
dispatch({ type: "CHANGE_FIELD", field, value })
}
const handleReset = () => {
// Сбрасываем форму через действие RESET
dispatch({ type: "RESET", initialState })
}
return (
<div>
<label>
Тема
<select
value={state.theme}
onChange={handleChange("theme")}
>
<option value="light">Светлая</option>
<option value="dark">Темная</option>
</select>
</label>
<label>
<input
type="checkbox"
checked={state.notifications}
onChange={handleChange("notifications")}
/>
Включить уведомления
</label>
<button onClick={handleReset}>Сбросить</button>
</div>
)
}
С помощью такого подхода вы сохраняете локальность состояния, но делаете его обновление более предсказуемым и управляемым.
Паттерны повторного использования логального состояния
Кастомные хуки (React) как способ переиспользования логики
Если вы замечаете одинаковую логику локального состояния в нескольких местах, ее можно вынести.
Давайте создадим хук для управления состоянием отображения модального окна.
// Кастомный хук для управления локальным состоянием модалки
import { useState, useCallback } from "react"
function useModal(initial = false) {
// Локальное состояние открытия модального окна
const [isOpen, setIsOpen] = useState(initial)
// Функция открытия
const open = useCallback(() => setIsOpen(true), [])
// Функция закрытия
const close = useCallback(() => setIsOpen(false), [])
// Функция переключения
const toggle = useCallback(
() => setIsOpen(prev => !prev),
[]
)
// Возвращаем состояние и методы управления
return { isOpen, open, close, toggle }
}
Теперь вы можете использовать useModal в разных компонентах, сохраняя состояние локальным для каждого из них:
function DeleteButton() {
const { isOpen, open, close } = useModal(false)
return (
<>
<button onClick={open}>Удалить</button>
{isOpen && (
<div>
{/* Здесь могла бы быть модалка */}
<p>Точно удалить элемент</p>
<button onClick={close}>Отмена</button>
<button onClick={close}>Удалить</button>
</div>
)}
</>
)
}
У каждого DeleteButton свое локальное состояние, но логика его обработки написана один раз.
Композиция в Vue
В Vue 3 похожая идея реализуется через композиционные функции. Вы выносите работу с локальным состоянием в отдельный модуль и подключаете ее к компонентам по мере необходимости.
Частые ошибки при работе с локальным состоянием
Ошибка 1: Избыточное дублирование локального и глобального состояния
Иногда разработчики дублируют одно и то же состояние:
- пользователь хранится в глобальном сторе
- при этом в компоненте создается еще одно локальное состояние
user, которое пытаются синхронизировать
Это создает рассинхронизацию: вы обновили глобальные данные, но забыли обновить локальные.
Решение:
- если данные важны на уровне всего приложения, не дублируйте их локально
- если данные важны только компоненту, не поднимайте их в глобальный стор
Ошибка 2: Локальное состояние, которое знает слишком много
Например, флаг isModalOpen в каком-то компоненте влияет на маршруты, глобальные уведомления и другие части интерфейса. Это означает, что, скорее всего, это состояние уже не локальное по смыслу, его стоит поднимать.
Ошибка 3: Состояние считается локальным, но меняется извне
Иногда компонент экспортирует функции, которые напрямую меняют его внутреннее состояние из других частей системы. В результате:
- теряется предсказуемость
- сложно понять, кто и когда меняет состояние
Лучше пробрасывать наружу только явно описанные интерфейсы (пропсы, события, колбэки), а не давать прямой доступ к внутренним переменным.
Практический пример сочетания локального и глобального состояния
Давайте соберем все вместе на небольшом примере.
Задача:
- глобально хранить текущего пользователя
- локально управлять формой изменения имени
Пусть у нас есть глобальный хук (или контекст, или стор), который возвращает пользователя и функцию обновления.
// Псевдохук useCurrentUser, который работает как интерфейс к глобальному состоянию
function useCurrentUser() {
// Здесь мог бы быть доступ к Redux, Zustand, Context или другому стору
// Для примера используем простую заглушку
const user = { id: 1, name: "Алексей" } // Глобальные данные пользователя
const updateUser = async (patch) => {
// Здесь мог бы быть запрос к API и обновление стора
// await api.updateUser(user.id, patch)
// dispatch(updatedUserAction(...))
// В примере просто выводим в консоль
console.log("Обновляем пользователя", patch)
}
return { user, updateUser }
}
Теперь создадим компонент, который использует глобальные данные, но управляет формой локально.
import { useState } from "react"
function ProfileNameEditor() {
// Получаем глобальные данные пользователя
const { user, updateUser } = useCurrentUser()
// Локальное состояние для поля ввода
const [name, setName] = useState(user.name)
// Локальное состояние для ошибки
const [error, setError] = useState(null)
// Локальное состояние для признака отправки
const [isSaving, setIsSaving] = useState(false)
const handleSubmit = async (event) => {
event.preventDefault()
setError(null)
if (!name.trim()) {
// Простейшая валидация
setError("Имя не может быть пустым")
return
}
setIsSaving(true)
try {
// Обновляем глобального пользователя через функцию из стора
await updateUser({ name })
} catch (e) {
setError("Не удалось сохранить изменения")
} finally {
setIsSaving(false)
}
}
return (
<form onSubmit={handleSubmit}>
<label>
Имя
<input
type="text"
value={name} // Локальное состояние формы
onChange={(e) => setName(e.target.value)}
/>
</label>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit" disabled={isSaving}>
{isSaving ? "Сохраняем..." : "Сохранить"}
</button>
</form>
)
}
Здесь хорошо видно разделение:
- глобальное состояние — пользователь как сущность
- локальное состояние — процесс редактирования (форма, ошибки, загрузка)
Заключение
Локальное состояние — это фундаментальный инструмент управления поведением интерфейса. Оно:
- изолирует данные, относящиеся к конкретному компоненту
- упрощает тестирование и рефакторинг
- снижает связанность разных частей приложения
- помогает начать разработку снизу вверх, не перегружая архитектуру
Ключевые практические моменты:
- храните только действительно необходимое состояние
- держите локальное состояние максимально близко к месту использования
- не дублируйте локальное и глобальное состояние без крайней необходимости
- структурируйте сложное локальное состояние (объекты, редьюсеры, композиция)
- выносите повторяемую логику локального состояния в переиспользуемые функции или хуки
Если вы сомневаетесь, делать состояние локальным или глобальным, чаще начинайте с локального. При росте требований вы всегда сможете поднять его на уровень выше или вынести в отдельный стор. Этот путь дает более чистую и предсказуемую архитектуру, чем преждевременное глобальное управление всем подряд.
Частозадаваемые технические вопросы по теме и ответы
Как передать локальное состояние дочернему компоненту и дать ему возможность его менять
Передайте значение и функцию обновления через пропсы.
// Родитель
const [value, setValue] = useState("")
<Child value={value} onChange={setValue} />
// Дочерний
function Child({ value, onChange }) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)} // Меняем состояние в родителе
/>
)
}
Как сбрасывать локальное состояние при смене маршрута или вкладки
Используйте размонтирование компонента. Разместите локальное состояние внутри компонента, который исчезает при смене маршрута, или меняйте ключ key, чтобы форсировать пересоздание компонента и его состояния.
// При изменении key React создаст компонент заново и сбросит локальное состояние
<ComponentWithLocalState key={routeId} />
Как сохранить локальное состояние между перезагрузками страницы
Сериализуйте состояние в localStorage или sessionStorage и инициализируйте из него.
const [value, setValue] = useState(() => {
// Читаем сохраненное значение один раз при инициализации
return localStorage.getItem("my-value") || ""
})
useEffect(() => {
// Сохраняем при каждом изменении
localStorage.setItem("my-value", value)
}, [value])
Как работать с локальным состоянием в асинхронных обработчиках без гонок
Используйте функциональное обновление, если новое значение зависит от предыдущего, и учитывайте размонтирование.
setCount(prev => prev + 1) // Безопасно при нескольких вызовах подряд
Для отмены асинхронных операций проверяйте флаг размонтирования или используйте AbortController.
Как протестировать логику локального состояния отдельно от UI
Вынесите логику в чистые функции или хуки.
- для чистых функций пишите обычные юнит тесты
- для хуков используйте специализированные утилиты (например, React Testing Library hooks) и проверяйте начальное значение, изменение при вызове и побочные эффекты через имитацию событий
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

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