🧱 Суть

Обычный useEffect запускается:

  1. после первого монтирования компонента
  2. после каждого изменения зависимостей

Например:

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

🔗 Связано