При доступе к переменной состояния из 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);
};
}, []);
...
}
Вы можете передать обратный вызов 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. Зависимости могут быть только состоянием или реквизитом.