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.
Related
I have a react app, and I have a two routes, /home and /items which renders ListComponent
Items have a bool property scrollHere, which I am using to scroll to a list item automatically and it works well with the code as shown below. The problem that I have is that when I go back to /home then it tries to scroll down to the position that list item was. How can I prevent this behaviour?
export const ListComponent = (items: ItemObject) => {
const itemToScroll = useRef<null | HTMLDivElement>(null);
const mounted = useRef(false);
useEffect(() => {
if (!mounted.current) {
mounted.current = true;
} else {
scrollToItem();
}
});
const scrollToItem = () => {
itemToScroll.current?.scrollIntoView({ behavior: "smooth" });
};
return (
<div>
<ul>
{
items.map(item =>
item.scrollHere ? (<li ref={itemToScroll}>{itemName}</li>) : <></>
}
</ul>
</div>
);
};
Made a codesandbox of the issue : https://codesandbox.io/s/serene-rosalind-fcitpd?file=/src/Photo.tsx
The real problem comes when you change scroll direction, the index photo will glitch a bit ...
I have a photo gallery:https://virgile-hasselmann.vercel.app/photos. I want to switch the photos when the user scrolls up or down. To do so I thought I'd use wheelEvent and the e.deltaY. It kinda works but I had to add a useDebounce function because if not it would glitch the gallery. Here you can see the custom hook :
function useDebounce<T>(value: T, delay?: number) {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
const setDebounce = (newValue: T) => {
setTimeout(() => setDebouncedValue(newValue), delay || 500);
};
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return { debouncedValue, setDebounce };
}
In the Gallery component, here is how I've implemented the wheel event and the debouncing :
// Init the data to display with the photos of the first category
const [photoIdx, setPhotoIdx] = useState(0);
const { debouncedValue, setDebounce } = useDebounce(photoIdx, 1000);
const setDebouncedIdx = (value: number) => {
setDebounce(value);
setPhotoIdx(debouncedValue);
};
const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
if (e.deltaY > 0) {
if (photoIdx < dataSelected!.length - 1) {
setDebouncedIdx(photoIdx + 1);
} else {
setDebouncedIdx(0);
}
} else {
if (photoIdx > 0) {
setDebouncedIdx(photoIdx - 1);
} else {
setDebouncedIdx(dataSelected!.length - 1);
}
}
};
But the result does not satisfy me the least: it's glitchy and not really responding the user's input + if you try to go back by scrolling back, it will first show you the next photo before going back to it. Hard to explain word but it's quite clear if you look at the index at the bottom left corner.
Perhaps there is a better way of implementing it. The problem is unclear to me if anyone could enlighten me that would much appreciated :)
I think you can use an eventListener to detect if the user is scrolling. You can detect the direction of the scrolling too. So if the user scrolls down then you replace the image with the next one. Or scroll up then replace it with the previous one.
The threshold is used to determine how much you want to scroll before taking an action.
useEffect(() => {
//Scroll listener to determine the scroll direction
const threshold = 0;
let lastScrollY = window.scrollY;
let scrollDir = "";
const updateScrollDir = () => {
const scrollY = window.scrollY;
if (Math.abs(scrollY - lastScrollY) < threshold) {
return;
}
scrollDir = (scrollY > lastScrollY ? "scrolling down" : "scrolling up");
lastScrollY = scrollY > 0 ? scrollY : 0;
if (scrollDir == "scrolling up") {
//Do something to display the previous image
}
if (scrollDir == "scrolling down") {
//Do something to display the next image
}
};
const onScroll = () => {
window.requestAnimationFrame(updateScrollDir);
};
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
}
},
I am using the react-resizable library to resize columns in a table.
Using the library's API, I can call onSortStart and onSortStop when resizing of the element has started and finished.
When I log in side the triggered callbacks I only see a log once when resizing has started and once when resizing has stopped - so far so good.
But what I want is to be able to return a boolean flag, true when resizing has started and false when resizing has stopped.
The problem is is that the state is changing every time my useStateful custom hook is re-created / rendering. So I get hundreds of updated versions of the variable isResizing.
How can I just make isResizing return true once when started and false once when stopped?
Here is my code:
const ResizableColumn = (props: ResizableColumnProps): ReactElement => {
const { onResizeStart, onResizeStop } = useStateful();
const {
onResize, width, className, children, style,
} = props;
if (!width) {
return <th className={className} style={style}>{children}</th>;
}
return (
<Resizable
width={width}
height={0}
onResize={onResize}
minConstraints={[150, 150]}
draggableOpts={{ enableUserSelectHack: false }}
onResizeStart={onResizeStart}
onResizeStop={onResizeStop}
>
<th className={className} style={style}>{children}</th>
</Resizable>
);
};
export default ResizableColumn;
useStateful:
const useStateful = (): any => {
const [isResizing, setIsResizing] = useState(false);
const onResizeStart = (): void => {
console.log('%c Resizing Started!', 'color: green;'); // logs once - perfect
setIsResizing(true);
};
const onResizeStop = (): void => {
console.log('%c Resizing Stopped!', 'color: green;'); // logs once - perfect
setIsResizing(false);
};
console.log('%c isResizing', 'color: green;', isResizing); // logs hundreds of times with different values
return { isResizing, onResizeStart, onResizeStop };
};
export default useStateful;
I have also tried using useRef but the same thing happens:
const useStateful = (): any => {
const isResizing = useRef<boolean>();
const onResizeStart = (): void => {
console.log('%c Resizing Started!', 'color: green;'); // logs once - perfect
isResizing.current = true;
};
const onResizeStop = (): void => {
console.log('%c Resizing Stopped!', 'color: green;'); // logs once - perfect
isResizing.current = false;
};
console.log('%c isResizing', 'color: green;', isResizing.current); // logs hundreds of times with different values
return { isResizing, onResizeStart, onResizeStop };
};
export default useStateful;
Ciao, I saw your code for a lot of time and the only thing I noted is that on your custom hook useStateful you are returning a different number of elements compared to the line in which you are using your custom hook. I made a small example here.
The custom hook is conceptually equal to yours:
const useStateful = () => {
const [isResizing, setIsResizing] = useState(false);
const onResizeStart = () => {
setIsResizing(true);
console.log("onResizeStart");
};
const onResizeStop = () => {
setIsResizing(false);
console.log("onResizeStop");
};
console.log(isResizing);
return [isResizing, onResizeStart, onResizeStop];
};
But, when I use this hook I wrote:
const [isResizing, onResizeStart, onResizeStop] = useStateful();
Thats it. I konw that my example could not be what you are looking for but it's a little bit tricky to recreate your environment.
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?
Let's see we have a parent, which can have unknown number of children of unknown components. In my specific example, I have a dropdown menu with absolute position because of dnd list-order change. And I have a parent which is the provider of this dnd feature. I have an useEffect on the parent, which calls a function at every update, which sets the styles of the children, and the parent as well. Here's how it's looking like:
const SortDrag = ({ children, onChange, onEnd }) => {
const sortDragContainer = useRef();
const setStyles = () => {
let top = 0;
const { children, style } = sortDragContainer.current;
[ ...children ].forEach(child => {
const { offsetHeight: height } = child;
child.style.transform = `translateY(${top}px)`;
top += height;
});
style.height = `${top}px`;
};
const Elements = children.map((child, i) => {
const { id } = child.props || child;
return (
<SortDragElement id={id} order={i} key={id}>
{child}
</SortDragElement>
);
}).sort((a, b) => a.order - b.order);
useEffect(() => {
if (onChange) sortDragContainer.current.onChange = onChange;
if (onEnd) sortDragContainer.current.onEnd = onEnd;
if (reverse) sortDragContainer.current.reverse = reverse;
}, []);
useEffect(() => { setStyles(); });
return (
<div
className="sort-drag-container"
ref={sortDragContainer}
>
{Elements}
</div>
);
};
export default SortDrag;
I have event listeners in a separate file to handle the dnd feature, but it's not related to this problem, just don't want this to cause confusion.
What I'd like to update the parent component every time a change happens in one of it's children.