I have a react component, HomeScreen.js:
import React, { useEffect,useState } from "react";
const HomeScreen = () => {
const [scrolledY, setScrolledY] = useState(0);
const handleScroll = () => {
const scrollY = window.scrollY;
setScrolledY(scrollY);
console.log(scrolledY);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
}, []);
return (
<div
className="homeScreenContainer"
>
</div>
</div>
);
};
Why is scrolledY not getting updated when I scroll?
Got it. I just had to add the scrolledY state as useEffect's dependency;
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [scrolledY]);
Related
just started to learn Next Js. I wanna hide dropdown when i clicked outside the button. Code works fine in create-react-app. But i tried to implement in nextjs, it doesnt working.
const LanguageRef = useRef();
const [languageDD, setLanguageDD] = useState(false);
console.log(languageDD);
useEffect(() => {
if (!languageDD) return;
const checkIfClickedOutside = (e) => {
if (
languageDD &&
LanguageRef.current &&
!LanguageRef.current.contains(e.target)
) {
setLanguageDD(false);
}
};
document.addEventListener("click", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("click", checkIfClickedOutside);
};
}, [languageDD]);
link tag
<a onClick={() => setLanguageDD((prev) => !prev)}>Language </a>
Does useEffect work in Nextjs?
Working Solution:
const LanguageRef = useRef();
const LanguageDDRef = useRef();
const [languageDD, setLanguageDD] = useState(false);
console.log(languageDD);
useEffect(() => {
console.log("useeffect")
if (!languageDD) return;
const checkIfClickedOutside = (e) => {
if (
languageDD &&
LanguageRef.current &&
!LanguageRef.current.contains(e.target) &&
LanguageDDRef.current &&
!LanguageDDRef.current.contains(e.target)
) {
setLanguageDD(false);
}
};
document.addEventListener("click", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("click", checkIfClickedOutside);
};
}, [languageDD]);
<a onClick={() => setLanguageDD((prev) => !prev) ref={LanguageDDRef}}>Language </a>
I reworked and also splitted the clickOutside function to a Custom Hook, because it can reuse at the other components. Like this:
import React, { useEffect } from 'react';
export const useClickOutside = (ref, handler) => {
useEffect(() => {
const listener = (e) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref?.current || ref.current.contains(e.target)) {
return;
}
handler();
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
};
}, [handler, ref]);
};
Finally, you just need import and use useClickOutside hook, like this:
import React, { useRef, useState } from 'react';
import { useClickOutside } from '../src/hooks';
export default function Home() {
const LanguageDDRef = useRef();
const [languageDD, setLanguageDD] = useState(true);
useClickOutside(LanguageDDRef, () => setLanguageDD(false));
const handleToggleButton = () => {
setLanguageDD((prev) => !prev);
};
return (
<button onClick={handleToggleButton} ref={LanguageDDRef}>
{languageDD ? 'Show' : 'Hide'}
</button>
);
}
I have this hook in React I want to write the unit test but I faced the problem that I don't know how I could cover handleScroll function, how can I go to the useEffect to trigger the scroll event?
I tried fireEvent.scroll but not success.
import { useState, useEffect } from 'react';
const scrollWindow = (
fetchOffset: number,
callback: () => void,
preventFetch?: boolean,
) => {
const [isFetching, setIsFetching] = useState(false);
const handleScroll = () => {
const scrollFromBottom = docElem.scrollHeight - docElem.scrollTop - docElem.clientHeight;
if (scrollFromBottom - fetchOffset > 0) return;
setIsFetching(true);
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [isFetching, preventFetch]);
useEffect(() => {
if (!isFetching) return;
callback();
}, [isFetching]);
return [isFetching, setIsFetching];
};
export default scrollWindow;
Added a change in the color of the appbar when scrolling, but the problem is that the useEffect changes the data array. Every time the color of the appbar changes, the array itself changes.
Can it be rewritten in some other way?
const colorChange = useCallback(() => {
if (window.scrollY >= 200) {
setColor(true)
} else {
setColor(false)
}
}, [])
useEffect(() => {
window.addEventListener('scroll', colorChange)
return () => window.removeEventListener('scroll', colorChange)
}, [colorChange])
The function I use for random arrays
import _ from 'lodash'
export const shuffle = (array) => {
const random = _.shuffle(array)
return random.slice(0, 2)
}
I found the answer. With ref data no longer changes due to scrolling.
I am attaching the solution:
import React, {useEffect, useRef, useState} from 'react'
const ref = useRef()
const [color, setColor] = useState(false)
ref.current = color
useEffect(() => {
const colorChange = () => {
const show = window.scrollY >= 200
if (ref.current !== show) {
setColor(true)
}
}
window.addEventListener('scroll', colorChange)
return () => window.removeEventListener('scroll', colorChange)
}, [])
You don't have to pass colorChange in dependencies of the useEffect. no need for the useCallback. you can define a function inside the useEffect only.
useEffect(() => {
const colorChange = () => setColor(window.scrollY >= 200);
window.addEventListener("scroll", colorChange);
return () => window.removeEventListener("scroll", colorChange);
}, []);
I'm currently fetching my data once when the component mounts, then whenever the user clicks a button. I want however to stop the button from fetching if a request is in progress, that's why I'm updating the isFetching state.
However, I need to add the isFetching to the useCallback dependency to remove the warning and if I do, an infinite fetch loop is triggered.
Here's my code:
import { useCallback, useEffect, useRef, useState } from 'react';
export const MyComponent = () => {
const isMounted = useRef(true);
const [isFetching, setIsFetching] = useState(false);
const [data, setData] = useState(null);
// Can also be called from the button click
const getMyData = useCallback(() => {
if (isFetching) return;
setIsFetching(true);
fetch('get/my/data')
.then((res) => {
if (isMounted.current) {
setData(res.data);
}
})
.catch((err) => {
if (isMounted.current) {
setData("Error fetching data");
}
})
.finally(() => {
if (isMounted.current) {
setIsFetching(false);
}
});
}, []); // isFetching dependency warning as is, if added then infinite loop
useEffect(() => {
isMounted.current = true;
getMyData();
return () => {
isMounted.current = false;
};
}, [getMyData]);
return (
<div>
<button onClick={getMyData}>Update data</button>
<p>{data}</p>
</div>
);
};
I understand that there are multiple questions like this one, but I couldn't remove the warning or infinite loop while still checking if the component is mounted.
Here are some examples: example-1, example-2
Convert isFetching to a ref, so it's value won't be a dependency of the function:
const { useCallback, useEffect, useRef, useState } = React;
const MyComponent = () => {
const isMounted = useRef(true);
const isFetching = useRef(false);
const [data, setData] = useState([]);
// Can also be called from the button click
const getMyData = useCallback(() => {
console.log('call');
if (isFetching.current) return;
isFetching.current = true;
fetch('https://cat-fact.herokuapp.com/facts')
.then(res => res.json())
.then(res => {
if (isMounted.current) {
setData(res);
}
})
.catch((err) => {
if (isMounted.current) {
setData("Error fetching data");
}
})
.finally(() => {
if (isMounted.current) {
isFetching.current = false;
}
});
}, []); // isFetching dependency warning as is, if added then infinite loop
useEffect(() => {
isMounted.current = true;
getMyData();
return () => {
isMounted.current = false;
};
}, [getMyData]);
return (
<div>
<button onClick={getMyData}>Update data</button>
<ul>
{
data.map(({ _id, text }) => (
<li key={_id}>{text}</li>
))
}
</ul>
</div>
);
};
ReactDOM.render(
<MyComponent />,
root
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
I have a custom hook that handles clicks outside of a component:
const useOutsideClick = (ref, callback) => {
const handleClick = e => {
if (ref.current && !ref.current.contains(e.target)) {
callback();
}
};
useEffect(() => {
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
});
};
I have used this hook in a component like this:
const MyComponent = () => {
const container = useRef();
const [isOpen, setIsOpen] = useState(false);
useOutsideClick(container, () => {
console.log("clicked outside");
});
return (
<>
<span>another element</span>
<button onClick={() => setIsOpen(false)}>click</button>
<div ref={container}></div>
</>
);
};
The problem is when I click on the span, everything works fine. But, when I click on the button an it updates the state, it doesn't enter useOutsideClick callback function. How can I solve this problem?
The issue here is that useEffect has no dependency array, so it is being called on every render. Also, it is better to keep the definition of handleClick inside the hook as it is only going to be used in the first render; otherwise if would be defined on every render.
const useOutsideClick = (ref, callback) => {
useEffect(() => {
const handleClick = e => {
if (ref.current && !ref.current.contains(e.target)) {
callback();
}
};
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
}, []);
};
I found that the best solution for useOutsideClick hook is this:
function useOutsideClick(ref, handler) {
useEffect(
() => {
const listener = event => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
},
[ref, handler]
);
}