javascript - Variável de dentro do Hook React useEffect será perdida após cada renderização
Eu tenho aqui uma animação de texto que está funcionando perfeitamente. O que eu quero adicionar agora é um Intersection Observer para que a animação só comece quando eu rolar até a Caixa.
Então, o que eu fiz para conseguir isso foi: usei o hook do React useRef para usar como referência para o elemento que quero observar e apliquei ao meu Box com ref={containerRef}. Depois, declarei uma função de callback que recebe um array de IntersectionObserverEntries como parâmetro; dentro dessa função, pego o primeiro e único entry e verifico se ele está intersectando com a viewport e, se estiver, então chama setIsVisible com o valor de entry.isIntersecting (true/false). Depois disso, adicionei o hook do React useEffect e criei um construtor de observer usando a função de callback e as opções que acabei de criar antes. Implementei a lógica em um novo hook que chamei de useElementOnscreen.
Está funcionando, MAS estou recebendo um aviso e não consigo resolvê-lo:
1. Arquivo tsx de Código Inicial
const useElementOnScreen = <T,>(options: T): [MutableRefObject<HTMLDivElement | null>, boolean] => {
const containerRef = useRef<HTMLDivElement | null>(null);
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries: IntersectionObserverEntry[]) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current) observer.observe(containerRef?.current);
return () => {
if (containerRef.current) observer.unobserve(containerRef?.current);
};
}, [containerRef, options]);
return [containerRef, isVisible];
};
Aqui recebo o aviso: if (containerRef.current) observer.unobserve(containerRef?.current);
O aviso é:
(property) MutableRefObject<HTMLDivElement | null>.current: HTMLDivElement
The ref value 'containerRef.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'containerRef.current' to a variable inside the effect, and use that variable in the cleanup function.
eslint react-hooks/exhaustive-deps
2. O que tentei fazer para remover o aviso foi salvar o valor atual da referência em uma variável de escopo local para ser encerrada dentro da função.
const useElementOnScreen = <T,>(options: T): [MutableRefObject<HTMLDivElement | null>, boolean] => {
let observerRefValue: Element | null = null; // <-- variable to hold ref value
const containerRef = useRef<HTMLDivElement | null>(null);
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries: IntersectionObserverEntry[]) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current) observer.observe(containerRef?.current);
observerRefValue = containerRef.current; // <-- save ref value
return () => {
if (observerRefValue) observer.unobserve(observerRefValue); // <-- use saved value
};
}, [containerRef, options]);
return [containerRef, isVisible];
};
Mas, novamente, aqui estou eu recebendo o aviso: observerRefValue=containerRef.current;/<-- salvar valor da referência
(property) MutableRefObject<HTMLDivElement | null>.current: HTMLDivElement | null
Assignments to the 'observerRefValue' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect.eslintreact-hooks/exhaustive-deps
3. Agora eu mudei meu let observerRefValue: Element | null=null; para const [observerRefValue, setObserverRefValue]=useState<Element | null>(null); E o aviso desapareceu e está funcionando! No entanto, no estado atual, não estou usando setObserverRefValue em nenhum lugar e estou recebendo o aviso 'setObserverRefValue' é atribuído a um valor mas nunca usado.
const useElementOnScreen = <T,>(options: T): [MutableRefObject<HTMLDivElement | null>, boolean] => {
const [observerRefValue, setObserverRefValue] = useState<Element | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries: IntersectionObserverEntry[]) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current) observer.observe(containerRef?.current);
return () => {
if (observerRefValue) observer.unobserve(observerRefValue); // <-- use saved value
};
}, [observerRefValue, containerRef, options]);
return [containerRef, isVisible];
};
É apenas um aviso, mas minha pergunta é:
Minha solução está correta para lidar com o aviso anterior? Ou talvez existam soluções melhores? E em relação ao 'setObserverRefValue' é atribuído um valor, mas nunca usado, existe uma maneira de usá-lo no meu exemplo para que eu possa me livrar do aviso?
Resposta #1
O que Adam postou é uma boa maneira de resolver isso. Eu uso exatamente o mesmo padrão de código na minha base de código. O ref não faz parte do hook e é passado como uma prop. Dessa forma, o hook contém apenas a lógica para verificar se o Node apontado pelo ref está visível na tela ou não. Isso funcionou para nós até agora.
Agora, no seu código, o hook também cuida do ref. E então você anexa o ref a qualquer valor retornado pelo hook. (Gostaria de saber nos comentários por que você está seguindo esse padrão.)
Vou discutir todos os três trechos de código que você forneceu:
- O useEffect
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current) observer.observe(containerRef?.current);
observerRefValue = containerRef.current; // <-- save ref value
return () => {
if (observerRefValue) observer.unobserve(observerRefValue); // <-- use saved value
};
}, [containerRef, options]);
Primeiramente, você pode usar containerRef como uma dependência, mas deixe-me lembrar que alterar containerRef.current não acionará uma renderização. Não é uma variável de state e não contribui para uma re-renderização. Então, se o ref mudar, você não pode ter certeza de que o callback do useEffect será executado novamente e, portanto, você pode ter valores incorretos no seu container ref.
O aviso de linting tem a mesma preocupação:
O valor da ref 'containerRef.current' provavelmente terá mudado quando esta função de limpeza do efeito for executada. Se esta ref aponta para um nó renderizado pelo React, copie 'containerRef.current' para uma variável dentro do efeito e use essa variável na função de limpeza.
Aqui está um link com uma discussão semelhante. Este sandbox de código da discussão é uma ótima demonstração.
function Box() {
const ref = useRef();
useEffect(() => {
console.log("mount 1 ", ref.current);
return () => setTimeout(() => console.log("unmount 1 ", ref.current), 0);
}, []);
useEffect(() => {
const element = ref.current;
console.log("mount 2 ", element);
return () => setTimeout(() => console.log("unmount 2 ", element), 0);
}, []);
return (
<div ref={ref} className="box">
Box
</div>
);
}
export default function App() {
let [state, setState] = useState(true);
useEffect(() => {
setTimeout(() => setState(false), 1000);
}, []);
return (
<div className="App">
<p>useEffect useRef warning</p>
{state && <Box />}
</div>
);
}
A saída para a primeira função de retorno do useEffect será unmount 1 null, porque o valor mudou, mas o callback do efeito não está ciente disso.
Manipulações semelhantes do DOM também podem acontecer no seu código. Agora que reconhecemos que o alerta faz sentido, vamos primeiro chegar à solução:
A solução simples:
const useOnScreen = <T extends IntersectionObserverInit>(options: T): [MutableRefObject<HTMLDivElement | null>, boolean] => {
const containerRef = useRef<HTMLDivElement | null>(null);
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries: IntersectionObserverEntry[]) => {
const [entry] = entries;
if(entry)
setIsVisible(entry.isIntersecting);
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
const refCopy = containerRef.current;
if (refCopy) observer.observe(refCopy);
return () => {
if (refCopy) observer.unobserve(refCopy);
};
}, [options]);
return [containerRef, isVisible];
};
O código acima resolve todos os avisos. Isso é exatamente o que o linter sugeriu. O ref aponta para um Elemento DOM, que ao desmontar se torna null, mas mantendo uma cópia da variável, ainda podemos acessá-la e fazer o que quisermos com ela.
Indo para suas outras tentativas:
O
observerRefValueé uma variável não especial (não é uma variávelrefnem uma variávelstate) aqui, e as alterações feitas nela serão apagadas a cada renderização. Portanto, você não pode confiar no seu valor. Após cada re-renderização, ele recebe o valornull, como definido inicialmente. Daí o aviso. Isso também pode não ajudar no seu caso de uso.Você está criando uma variável de estado extra aqui. Ela é inicializada com
null, mas nunca é atualizada para qualquer outro valor. A condição abaixo sempre será falsa e seu código pretendido não será executado apenas:
if (observerRefValue)
Isso também não ajudará no seu caso de uso. Além disso, você não deve usar uma variável de estado se não for necessário.
Resposta #2
Eu acho que poderia ser algo como
export function useElementOnScreen(ref: RefObject<HTMLElement>, rootMargin?: string) {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
setIsVisible(entry.isIntersecting);
},
{ threshold: [0, 1], rootMargin }
);
observer.observe(element);
return () => {
observer.disconnect();
};
}, [ref, rootMargin]);
return isVisible;
}
Este hook retorna boolean indicando se o elemento está visível ou não.
Observe que o hook não cria o ref por si só - em vez disso, ele espera recebê-lo como um argumento.
Normalmente, é um padrão melhor, já que você pode querer executar múltiplos hooks no mesmo elemento. Se cada hook criasse um ref independente, então não haveria uma maneira fácil de atribuir todos esses refs a um único elemento HTML.
Exemplo de uso:
export function Foo() {
const elementRef = useRef<HTMLElement>(null);
const isVisible = useIsVisible(elementRef);
const isVisibleWithMargin = useIsVisible(elementRef, "100px");
return (
<div ref={elementRef}>
{isVisible ? "visible" : "not visible"}
{isVisibleWithMargin ? "visible with margin" : "not visible with margin"}
</div>
);
}
Observe que estou salvando o valor de ref.current em const element. Isso ocorre porque ref.current é mutável e pode ser limpado pelo React quando o elemento é desmontado. Portanto, precisamos salvá-lo em uma variável para podermos limpá-lo corretamente mais tarde.
