🧱 Суть
Обычный useEffect запускается:
- после первого монтирования компонента
- после каждого изменения зависимостей
Например:
useEffect(() => {
storage.set('notes', notes);
}, [notes]);Проблема:
при первом открытии компонента notes уже существует, поэтому эффект сразу выполнится.
Но иногда нужно:
выполнить действие только после изменения данных, а первый запуск пропустить
📦 Проблема
Представим:
const [notes, setNotes] = useState(() => {
return storage.get('notes');
});При первом рендере:
компонент загрузился
↓
notes получили из storage
↓
useEffect запускается
↓
storage.set('notes', notes)Получается лишняя запись.
Нам нужно:
первый запуск
↓
ничего не делать
изменение notes
↓
сохранить данные✅ Решение
Использовать useRef как флаг:
const didMount = useRef(false);💻 Код
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) {
storage.set('notes', notes);
} else {
didMount.current = true;
setLoading(false);
}
}, [notes]);Первый запуск
Компонент монтируется:
didMount.current === falseЗаходим сюда:
else {
didMount.current = true;
setLoading(false);
}Что произошло:
- запись в storage не выполнена
- флаг переключился
- компонент запомнил, что первый запуск уже был
Второй и последующие запуски
Например:
setNotes(newNotes);React вызывает эффект снова:
didMount.current === trueТеперь:
storage.set('notes', notes);выполнится.
Почему используется useRef
Можно было бы использовать useState, но:
const [didMount, setDidMount] = useState(false);создал бы лишний ререндер.
useRef:
const didMount = useRef(false);создаёт объект:
{
current: false
}Который:
- сохраняется между рендерами
- изменяется без нового рендера
- идеально подходит для флагов
Почему нельзя просто проверить notes
Например:
useEffect(() => {
if (notes) {
storage.set('notes', notes);
}
}, [notes]);Это не решает проблему.
Потому что при первом рендере:
useState получил notes
↓
notes уже существует
↓
useEffect запускается
↓
storage.set()То есть первый вызов всё равно произойдёт.
Кастомный хук
Чтобы не повторять этот паттерн:
const useDidMountEffect = (
callback: () => void,
deps: DependencyList
) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) {
callback();
} else {
didMount.current = true;
}
}, deps);
};Теперь:
useDidMountEffect(() => {
storage.set('notes', notes);
}, [notes]);Поведение:
mount
↓
пропустить
update
↓
выполнить callback🎯 Вывод
- обычный
useEffectсрабатывает после первого mount - иногда первый запуск нужно пропустить
useRefудобно использовать как флаг состояния жизненного циклаdidMount.current = trueозначает «первый рендер уже прошёл»- этот паттерн используется для сохранения данных, синхронизации с API и localStorage