Hi i cannot add scroll listener by id, it works with WINDOW but doesnt work in custom scroll element.
Here is my code :
componentDidMount() {
const scrollPanel = document.getElementById('scrollPanel');
scrollPanel.addEventListener('scroll', this.listenToScroll);
}
componentWillUnmount() {
const scrollPanel = document.getElementById('scrollPanel');
scrollPanel.removeEventListener('scroll', this.listenToScroll);
}
listenToScroll = () => {
const { position } = this.state;
const scrollPanel = document.getElementById('scrollPanel');
const winScroll = scrollPanel.scrollTop;
const height =
scrollPanel.scrollHeight -
scrollPanel.clientHeight;
const scrolled = winScroll / height;
console.log('scrolled', scrolled);
this.setState({
position: scrolled,
});
When i try to check some value its never changes
There must be an event handler inside the element which creates the scrolling element:
Here in your case, change the componentDidMount() to below:
componentDidMount() {
document.addEventListener(
"scroll",
this.listenToScroll,
true // Capture event
);
}
listenToScroll = () => {
//Your custom code
alert("click triggered");
};
I tried and it works
You might want to check this answer where I got the reference from :
https://stackoverflow.com/a/30475606
Related
I'm trying to build an infinite scroll component in React (specifically using NextJS). I am having trouble with this feature because when I set a scroll event on the window, it doesn't have access to updated state. How can I write a scroll event that listens to any scrolling on the entire window that also has access to state like router query params?
Here's some code to see what I'm trying to do:
useEffect(() => {
window.addEventListener('scroll', handleScroll);
},[]);
const handleScroll = () => {
const el = infiniteScroll.current;
if (el) {
const rect = el.getBoundingClientRect();
const isVisible =
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <=
(window.innerWidth || document.documentElement.clientWidth);
if (isVisible && !isComplete && !isFetching) {
nextPage();
}
}
};
const nextPage = () => {
const params = router.query as any; // <------ these params here never update with state and are locked in to the the values they were at when the component mounted
params.page = params.page
? (parseInt((params as any).page) + 1).toString()
: '1';
router.replace(router, undefined, { scroll: false });
};
The issue is that the router value is locked at the place it was when the component mounted.
I've tried removing the empty array of dependencies for the useEffect at the top, but as you can imagine, this creates multiple scroll listeners and my events fire too many times. I've tried removing the eventListener before adding it every time, but it still fires too many times.
Every example I've found online seems to not need access to state variables, so they write code just like this and it works for them.
Any ideas how I can implement this?
I've tried to use the onScroll event, but it doesn't work unless you have a fixed height on the container so that you can use overflow-y: scroll.
You can use a ref to access and modify your state in the scope of the handleScroll function.
Here is how:
const yourRef = useRef('foo');
useEffect(() => {
const handleScroll = () => {
const value = yourRef.current;
if (value === 'foo') {
yourRef.current = 'bar'
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
I figured something out that works. Posting in case anyone else is having the same issue.
I created a custom hook called useScrollPosition that sets a listener on the window and updates the scroll position. It looks like this:
const useScrollPosition = () => {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const updatePosition = () => {
setScrollPosition(window.pageYOffset);
};
window.addEventListener('scroll', updatePosition);
updatePosition();
return () => window.removeEventListener('scroll', updatePosition);
}, []);
return scrollPosition;
};
and using that in my component like this:
useEffect(() => {
handleScroll();
}, [scrollPosition]);
allows me to access the current state of the router
I'm trying to implement infinite scrolling in my private messages page. So when user scroll to top (in the div) it will load more messages.
I have a problem in the first load.
It loads all the messages before scroll is happening.
There is a way to make the scrollbar start from the bottom?
I'm using Redux-Saga to load more messages for conversation component (loadMoreMessages) and the reducer updates messages state.
Here is the code:
const ConversationComp = (props) => {
const {
loadMoreMessages,
messages
} = props;
const ref = useRef(null);
useLayoutEffect(() => {
const elem = ref.current;
if (elem) {
console.log("scrolling to bottom");
elem.scrollTop = elem.scrollHeight;
}
}, []);
useEffect(() => {
console.log("loading more messages");
loadMoreMessages();
}, []);
const onScrollHandler = (e) => {
const elem = e.target;
const threshold = 30;
if (elem.scrollTop < threshold) {
// reached top
loadMoreMessages();
}
};
return (
<div onScroll={onScrollHandler} ref={ref}>
{
messages.map(m => {
...
})
}
</div>
);
}
The desired behavior:
Load first batch of messages
scrollbar is at the bottom
When user scroll to top it loads more messages
Any ideas?
Hello I'm trying to pass the following code to reacthooks:
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';
class SomeComponent extends React.Component {
// 2. Initialise your ref and targetElement here
targetRef = React.createRef();
targetElement = null;
componentDidMount() {
// 3. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav).
// Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element).
// This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired.
this.targetElement = this.targetRef.current;
}
showTargetElement = () => {
// ... some logic to show target element
// 4. Disable body scroll
disableBodyScroll(this.targetElement);
};
hideTargetElement = () => {
// ... some logic to hide target element
// 5. Re-enable body scroll
enableBodyScroll(this.targetElement);
}
componentWillUnmount() {
// 5. Useful if we have called disableBodyScroll for multiple target elements,
// and we just want a kill-switch to undo all that.
// OR useful for if the `hideTargetElement()` function got circumvented eg. visitor
// clicks a link which takes him/her to a different page within the app.
clearAllBodyScrollLocks();
}
render() {
return (
// 6. Pass your ref with the reference to the targetElement to SomeOtherComponent
<SomeOtherComponent ref={this.targetRef}>
some JSX to go here
</SomeOtherComponent>
);
}
}
And then I did the following with hooks:
const [modalIsOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
const targetRef = useRef();
const showTargetElement = () => {
disableBodyScroll(targetRef);
};
const hideTargetElement = () => {
enableBodyScroll(targetRef);
};
useEffect(() => {
if (modalIsOpen === true) {
showTargetElement();
} else {
hideTargetElement();
}
}, [modalIsOpen]);
I don't know if I did it correctly with useRef and useEffect, but it worked, but I can't imagine how I'm going to get to my componentWillUnmount to call mine:
clearAllBodyScrollLocks ();
The basic equivalents for componentDidMount and componentWillUnmount in React Hooks are:
//componentDidMount
useEffect(() => {
doSomethingOnMount();
}, [])
//componentWillUnmount
useEffect(() => {
return () => {
doSomethingOnUnmount();
}
}, [])
These can also be combined into one useEffect:
useEffect(() => {
doSomethingOnMount();
return () => {
doSomethingOnUnmount();
}
}, [])
This process is called effect clean up, you can read more from the documentation.
My drag and drop is very slow because of too many re-renders.
React.memo doesn't seem to help although I passed all items as primitives.
My list looks as follows:
const TabList = ({ selectedTabsState, handleItemSelect, windowId, windowIndex, tabs, actions }) => {
const { dragTabs } = actions;
const moveTabs = ({ dragWindowId, dragTabIndex, hoverWindowId, hoverTabIndex, draggedTabs }) => {
dragTabs({
fromWindowId: dragWindowId,
dragTabIndex,
toWindowId: hoverWindowId,
hoverTabIndex,
draggedTabs
});
};
const ref = useRef(null);
// We need this to fix the bug that results from moving tabs from one window to a previous
const [, drop] = useDrop({
accept: ItemTypes.TAB,
hover(item, monitor) {
if (!ref.current) {
return
}
const dragWindowId = item.windowId;
const dragTabIndex = item.tabIndex;
const hoverWindowId = windowId;
if (hoverWindowId > dragWindowId) {
return;
}
const hoverTabIndex = tabs.length;
moveTabs({ dragWindowId, dragTabIndex, hoverWindowId, hoverTabIndex, draggedTab: item.tab });
item.windowId = hoverWindowId;
item.tabIndex = hoverTabIndex;
}
});
drop(ref);
const renderTab = (tab, index) => {
const isSelected = selectedTabsState.selectedTabs.find(selectedTab => selectedTab.id === tab.id);
return (
<TabListItem
key={`tab_${windowId}_${tab.id}`}
windowId={windowId}
windowIndex={windowIndex}
tabIndex={index}
isSelected={ isSelected }
moveTabs={moveTabs}
handleItemSelection={ handleItemSelect }
tabId={ tab.id }
tabUrl={ tab.url }
tabTitle={ tab.title }
/>)
};
return (
<li>
<ul className="nested-list">
{ tabs.map((tab, index) => renderTab(tab, index)) }
</ul>
<div ref={ ref } className='nested-list-bottom'></div>
</li>
);
};
const mapDispatchToProps = (dispatch) => {
return {
actions: bindActionCreators(
Object.assign({}, CurrentWindowsActions)
, dispatch)
}
};
const mapStateToProps = state => {
return {
selectedTabsState: state.selectedTabs
};
};
export default connect(mapStateToProps, mapDispatchToProps)(TabList);
My list item looks as follows:
const collect = (connect, monitor) => ({
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource(),
// You can ask the monitor about the current drag preview
connectDragPreview: connect.dragPreview(),
// You can ask the monitor about the current drag state:
isDragging: monitor.isDragging(),
});
// We use dragSource to add custom isDragging
/* const tabSource = {
beginDrag({ selectedTabsState }) {
return { selectedTabs: selectedTabsState.selectedTabs };
}
}; */ // --> this is also problematic... I can never pass selectedTabsState to the item to be used in the drag layer, because it will re-render all items as well, and it is required to be passed as parameter to DragSource.
const tabSource = {
beginDrag() {
return {selectedTabs: [{id: 208}]};
}
};
const TabListItem = React.memo(
({ connectDragPreview, isSelected, handleItemSelection, connectDragSource, isDragging, windowId, windowIndex, tabId, tabUrl, tabTitle, tabIndex, moveTabs }) => {
useEffect(() => {
// Use empty image as a drag preview so browsers don't draw it
// and we can draw whatever we want on the custom drag layer instead.
connectDragPreview(getEmptyImage(), {
// IE fallback: specify that we'd rather screenshot the node
// when it already knows it's being dragged so we can hide it with CSS.
captureDraggingState: true
});
}, []);
const ref = useRef(null);
const [, drop] = useDrop({
accept: ItemTypes.TAB,
hover(item, monitor) {
if (!ref.current) {
return
}
const dragWindowId = item.windowId;
const dragTabIndex = item.tabIndex;
const hoverWindowId = windowId;
const hoverTabIndex = tabIndex;
// Don't replace items with themselves
if (dragTabIndex === hoverTabIndex && dragWindowId === hoverWindowId) {
return
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current.getBoundingClientRect();
// Get vertical middle
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragTabIndex < hoverTabIndex && hoverClientY < hoverMiddleY) {
return
}
// Dragging upwards
if (dragTabIndex > hoverTabIndex && hoverClientY > hoverMiddleY) {
return
}
// Time to actually perform the action
moveTabs({ dragWindowId, dragTabIndex, hoverWindowId, hoverTabIndex, draggedTabs: item.selectedTabs });
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
item.tabIndex = hoverTabIndex;
}
});
drop(ref);
console.log('render');
return connectDragSource(
<li ref={ ref }
style={ getTabStyle(isDragging, isSelected) }
onClick={(e) => handleItemSelection(e.metaKey, e.shiftKey, tabId, tabIndex)}
>
<div className='nested-list-item'>
<div>{ tabTitle }</div>
<a className='url' target="_blank" href={tabUrl}>{tabUrl}</a>
</div>
</li>
);
});
export default DragSource(ItemTypes.TAB, tabSource, collect)(TabListItem);
The code only drags selected items at a time (this must be shown in the custom drag layer); it doesn't throw exceptions (it works), but it is slow as hell.
In my console I can see that the item is rendered 48 times, which is the number of list items I have. This makes the drag very choppy and would become increasingly choppy with more list items.
Any idea why React.memo doesn't work in my case?
Edit: I found that part of the choppiness comes from the fact that drop-hover code is not correctly calculated anymore when it concerns multiple list items being dragged. Doesn't take away the fact that it shouldn't need to re-render all list-items on dragging just a few.
I'm not sure why the re-renders of all items still happen despite using React.memo, but I got rid of the choppiness by fixing the following code:
const tabSource = {
beginDrag({ selectedTabsState, windowId, tabIndex }) {
return { windowId, tabIndex, selectedTabs: selectedTabsState.selectedTabs };
}
};
This caused the drag-hover calculation to trigger too many state updates, followed by a ton of re-renders when redux updated the state.
I have created a function to detect scroll status, means if the user has scrolled to the bottom of the page then 'console.log(true)' and setting state. The function name is handleScroll and I am calling that function from helper file. And in my view file, I'm calling event listener to detect scroll change using the handleScroll function inside componentDidMount & later removing event listener by unmounting.
However, when I run the code initially state is set inside 'atBottom: false'. But later if I scroll down the page the function is not called again and I can't detect whether I am bottom of the page or not.
----> View file
import { handleScroll } from 'components/Helper.jsx'
class ScrollStatus extends Component {
constructor(props) {
super(props);
this.state = {
height: window.innerHeight,
scrollBottomStatus: false,
}
}
componentDidMount() {
window.addEventListener("scroll", handleScroll(this,
this.stateHandler));
}
componentWillUnmount() {
window.removeEventListener("scroll", handleScroll(this,
this.stateHandler));
}
stateHandler = (state) => {
this.setState(state);
}
render() {
return ( <div> Long text ... </div> ) }
}
export default ScrollStatus
----> helper file
export const handleScroll = (obj, stateHandler) => {
const windowHeight = "innerHeight" in window ? window.innerHeight :
document.documentElement.offsetHeight;
const body = document.body;
const html = document.documentElement;
const docHeight = Math.max(body.scrollHeight, body.offsetHeight,
html.clientHeight, html.scrollHeight, html.offsetHeight);
const windowBottom = Math.round(windowHeight + window.pageYOffset);
if (windowBottom >= docHeight) {
console.log(true)
stateHandler({
scrollBottomStatus: true
});
} else {
console.log(false)
stateHandler({
scrollBottomStatus: false
});
}
}
I want the function to keeping checking window height as I scroll down or up and keep updating the state 'isBottom' while scrolling.
I would appreciate the help.
When I check scrolling I always add a throttle (via lodash or ...) to throttle down the actions.
What I would do in your case.
1. Add eventlistener on mount, also remove on unmount.
componentDidMount = () => {
window.addEventListener('scroll', () => _.throttle(this.handleScroll, 100));
}
2. In the same component I'd handle the state update.
handleScroll = () => {
let scrollY = window.pageYOffset;
if(scrollY < 100) { this.setState({ // BLA })
}