Урок 10

Паттерны состояния

Несколько проверенных приёмов организации состояния.

Один источник истины

Состояние хранится в одном месте. Производные данные вычисляются, а не дублируются.

// Плохо
const [items, setItems] = useState<Item[]>([]);
const [total, setTotal] = useState(0);

// Хорошо
const [items, setItems] = useState<Item[]>([]);
const total = items.reduce((sum, i) => sum + i.price, 0);

Производное состояние

Не храни в стейте то, что можно вычислить.

// Плохо
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

// Хорошо
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;

Нормализация

Если данные вложенные — храни их плоско.

// Плохо
const [posts, setPosts] = useState<Post[]>([]);
// post.author.name — дублируется в каждом посте

// Хорошо
const [posts, setPosts] = useState<Record<number, Post>>({});
const [users, setUsers] = useState<Record<number, User>>({});
// post.authorId — ссылка

Разделение состояния

Разделяй независимые части. Не пихай всё в один объект.

// Плохо
const [state, setState] = useState({
    user: null,
    theme: 'light',
    sidebarOpen: false,
    notifications: [],
});

// Хорошо
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [sidebarOpen, setSidebarOpen] = useState(false);

Флаги vs статусы

Вместо нескольких булевых флагов — один статус.

// Плохо
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);

// Хорошо
const [status, setStatus] = useState<'idle' | 'loading' | 'error' | 'success'>('idle');

Невозможные состояния становятся невозможными.

Группировка связанных обновлений

Если несколько значений меняются вместе — объединяй.

const [form, setForm] = useState({ name: '', email: '' });

Reducer для сложных переходов

Если логика обновления сложная — useReducer.

const [state, dispatch] = useReducer(reducer, initialState);

Локальный vs глобальный

Держи состояние как можно ближе к использованию:

  1. В компоненте — если нужно только ему
  2. В родителе — если нужно соседям
  3. В контексте — если глубоко в дереве
  4. В сторе — если глобально

URL как состояние

Фильтры, сортировка, пагинация — лучше в URL:

const [searchParams, setSearchParams] = useSearchParams();
const page = Number(searchParams.get('page') ?? 1);

Плюсы: можно поделиться ссылкой, работают кнопки назад/вперёд.

Форма как состояние

Для форм — библиотека React Hook Form. Меньше ререндеров, встроенная валидация.

Серверное состояние

Не храни серверные данные в своём стейте. TanStack Query или SWR.

Итоги

  • Один источник истины
  • Производное не храни
  • Нормализуй вложенное
  • Разделяй независимое
  • Статус вместо флагов
  • URL — для фильтров
  • Серверное — в Query