При доступе к переменной состояния из useCallback значение не обновляется

В определенном месте моего кода я обращаюсь к переменной состояния моего компонента из обратного вызова (UserCallback), и я обнаружил, что переменная состояния не обновилась по сравнению с начальным значением, и обратный вызов относится к начальному значению. Как я читал в документации, когда переменная передается как один из элементов массива, она должна обновлять функцию при ее обновлении. Ниже приведен пример кода.


const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  const node = useRef(null);

  useImperativeHandle(ref, () => ({
    increment() {
      setCount(count + 1);
    }
  }));

  const clickListener = useCallback(
    e => {
      if (!node.current.contains(e.target)) {
        alert(count);
      }
    },
    [count]
  );

  useEffect(() => {
    // Attach the listeners on component mount.
    document.addEventListener("click", clickListener);
    // Detach the listeners on component unmount.
    return () => {
      document.removeEventListener("click", clickListener);
    };
  }, []);

  return (
    <div
      ref={node}
      style={{ width: "500px", height: "100px", backgroundColor: "yellow" }}
    >
      <h1>Hi {count}</h1>
    </div>
  );
});

const Parent = () => {
  const childRef = useRef();

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={() => childRef.current.increment()}>Click</button>
    </div>
  );
};

export default function App() {
  return (
    <div className="App">
      <Parent />
    </div>
  );
}


Изначально я создаю настраиваемый модальный режим подтверждения. У меня есть переменная состояния, которая устанавливает либо display:block, либо display:none для корневого элемента. Затем, если есть щелчок за пределами компонента, мне нужно закрыть модальное окно, установив для переменной состояния значение false. Ниже приводится исходная функция.

  const clickListener = useCallback(
    (e: MouseEvent) => {
      console.log('isVisible - ', isVisible, ' count - ', count, ' !node.current.contains(e.target) - ', !node.current.contains(e.target))
      if (isVisible && !node.current.contains(e.target)) {
        setIsVisible(false)
      }
    },
    [node.current, isVisible],
  )

Он не закрывается, потому что isVisible всегда имеет значение false, которое является начальным значением.

Что я здесь делаю не так?

Для дальнейших пояснений ниже приводится полный компонент.

const ConfirmActionModal = (props, ref) => {

  const [isVisible, setIsVisible] = useState(false)
  const [count, setCount] = useState(0)

  const showModal = () => {
    setIsVisible(true)
    setCount(1)
  }

  useImperativeHandle(ref, () => {
    return {
      showModal: showModal
    }
  });

  const node = useRef(null)
  const stateRef = useRef(isVisible);

  const escapeListener = useCallback((e: KeyboardEvent) => {
    if (e.key === 'Escape') {
      setIsVisible(false)
    }
  }, [])

  useEffect(() => {
    stateRef.current = isVisible;
  }, [isVisible]);

  useEffect(() => {
    const clickListener = e => {
      if (stateRef.current && !node.current.contains(e.target)) {
        setIsVisible(false)
      }
    };

    // Attach the listeners on component mount.
    document.addEventListener('click', clickListener)
    document.addEventListener('keyup', escapeListener)
    // Detach the listeners on component unmount.
    return () => {
      document.removeEventListener('click', clickListener)
      document.removeEventListener('keyup', escapeListener)
    }
  }, [])

  return (
    <div ref={node}>
      <ConfirmPanel style={{ display : isVisible ? 'block': 'none'}}>
        <ConfirmMessage>
          Complete - {isVisible.toString()} - {count}
        </ConfirmMessage>
        <PrimaryButton
          type="submit"
          style={{
            backgroundColor: "#00aa10",
            color: "white",
            marginRight: "10px",
            margin: "auto"
          }}
          onClick={() => {console.log(isVisible); setCount(2)}}
        >Confirm</PrimaryButton>
      </ConfirmPanel>
    </div>

  )

}

export default forwardRef(ConfirmActionModal)

3 ответа

Решение

Вы назначаете функцию clickListener к document.addEventListener при установке компонентов эта функция закрывается наcount значение.

На следующем рендере count значение будет устаревшим.

Один из способов решить эту проблему - реализовать вместо этого функцию с закрытием ссылки:

const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    // countRef.current always holds the most updated state
    const clickListener = e => {
      if (!node.current.contains(e.target)) {
        alert(countRef.current);
      }
    };

    document.addEventListener("click", clickListener);
    return () => {
      document.removeEventListener("click", clickListener);
    };
  }, []);
...
}

Редактировать fast-wood-stsrn

Вы можете передать обратный вызов setIsvisible, поэтому вам не нужно isVisible как зависимость от useCallback. Добавлениеnode.current бессмысленно, так как node является ссылкой и изменяется:

const clickListener = useCallback((e) => {
  setIsVisible((isVisible) => {//pass callback to state setter
    if (isVisible && !node.current.contains(e.target)) {
      return false;
    }
    return isVisible;
  });
}, []);//no dependencies needed

Пока твой clickListener действительно меняется, когда count изменения вы привязываете только к начальному clickListener однажды на горе, потому что ваш useEffectСписок зависимостей пуст. Вы могли бы рекламироватьclickListener в список зависимостей:

useEffect(() => {
    // Attach the listeners on component mount.
    document.addEventListener("click", clickListener);
    // Detach the listeners on component unmount.
    return () => {
      document.removeEventListener("click", clickListener);
    };
}, [clickListener]);

Боковое примечание: использование node.currentв списке зависимостей ничего не делает, так как react не замечает никаких изменений в ref. Зависимости могут быть только состоянием или реквизитом.

Другие вопросы по тегам