Современные React-приложения стали сложнее, но принципы эффективного управления состоянием остаются фундаментальными. Одна из самых распространённых ошибок, с которыми сталкиваюсь в ревью кода — избыточное состояние (state duplication). Хранение вычисляемых данных в состоянии создает точки отказа, усложняет логику и провоцирует трудноуловимые баги. Рассмотрим проблему, её последствия и решения на реальных примерах.
Типичный антипаттерн: Дублирование логики
Представьте компонент списка пользователей с фильтрацией:
function UserList() {
const [users, setUsers] = useState([]); // Исходные данные
const [searchTerm, setSearchTerm] = useState('');
const [filteredUsers, setFilteredUsers] = useState([]); // 🚫 Избыточное состояние!
useEffect(() => {
fetchUsers().then(data => setUsers(data));
}, []);
useEffect(() => {
// Фильтрация при изменении users или searchTerm
const filtered = users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filtered); // 😟 Дублируем данные
}, [users, searchTerm]);
return (
<>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{filteredUsers.map(user => <UserItem key={user.id} {...user} />)}
</>
);
}
Почему это проблема:
- Нарушение синхронизации: Риск расхождения между
users
иfilteredUsers
при асинхронных операциях. - Ненужные ререндеры: При изменении
users
сначала рендерится компонент с устаревшимиfilteredUsers
, затем повторно — послеuseEffect
. - Усложнение логики: Придется синхронизировать состояния для обновлений, удалений и пагинации.
Решение: Производные состояния через useMemo
function UserList() {
const [users, setUsers] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => { fetchUsers().then(setUsers); }, []);
const filteredUsers = useMemo(() => { // ✅ Вычисляемое значение
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]); // Зависимости кэширования
return ( /* JSX без изменений */ );
}
Как это работает:
filteredUsers
становится производным значением, рассчитываемым непосредственно изusers
иsearchTerm
.useMemo
кэширует результат, пока не изменятся зависимости, избегая избыточных вычислений.- Исчезает риск рассинхронизации — данные всегда отвечают текущим
users
иsearchTerm
.
Когда состояние действительно необходимо
- Данные, меняющиеся независимо от пропсов/состояния: Например, флаг
isOpen
для модального окна. - Промежуточное состояние формы, которое не является производным от других данных инпута.
- Оптимизированные тяжелые вычисления — если
useMemo
становится узким местом, возможно, потребуется реструктуризация или мемоизация на уровне селекторов (например, через Reselect).
Неочевидные пограничные случаи
Форма с валидацией:
const [email, setEmail] = useState('');
const [isEmailValid, setIsEmailValid] = useState(false); // ❌ Добавили состояние
// Вместо этого:
const isEmailValid = useMemo(() => isValidEmail(email), [email]); // ✅
Переключение элементов:
const [selectedItems, setSelectedItems] = useState([]);
const isItemSelected = useCallback(
(id) => selectedItems.includes(id),
[selectedItems]
); // ✅ Чистая функция без нового состояния
Оптимизации и производительность
useMemo
не гарантирует предотвращения вычислений — React может удалить кэш. Для тяжёлых операций это допустимая плата за согласованность.- Селекторы в Redux Toolkit: Используйте
createSelector
для мемоизации в хранилищах:javascriptconst selectFilteredUsers = createSelector( [selectUsers, selectSearchTerm], (users, term) => users.filter(u => u.name.includes(term)) );
- React Query / SWR: При работе с удалёнными данными делегируйте кэширование и синхронизацию этим библиотекам.
Какие проблемы это предотвращает
- Отладочные кошмары: Нет состояний-дубликатов — стек вызовов проще.
- Race conditions: Асинхронное обновление
filteredUsers
послеusers
может привести к ошибкам, если компонент размонтируется. - Тестирование: Компонент становится чистой функцией от
users
иsearchTerm
— проще покрывать тестами.
Выводы
Состояние в React должно хранить минимально необходимые данные, а не их производные. Старайтесь следовать принципу SSOT (Single Source of Truth). Прежде чем добавлять useState
, спросите:
- Могу ли я вычислить это значение из существующего состояния/пропсов?
- Часто ли оно меняется независимо от исходных данных?
- Что будет, если это значение временно рассинхронизируется?
Переход от дублирования состояния к стратегии вычисляемых значений не только сокращает код, но и фундаментально снижает энтропию вашего приложения. Декларативность не всегда интуитивна, но когда данные целостны и синхронны — сложные компоненты становятся предсказуемыми.