React dnd 使用自定义拖动层重新渲染太多,react.memo
React dnd too many re-renders with custom drag layer, react.memo
由于重新渲染太多,我的拖放速度很慢。
React.memo
似乎没有帮助,尽管我将所有项目都作为基元传递。
我的列表如下所示:
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);
我的列表项如下所示:
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);
代码一次只拖动选定的项目(这必须在自定义拖动层中显示);它不会抛出异常(它有效),但它非常慢。
在我的控制台中,我可以看到该项目被渲染了 48 次,这是我拥有的列表项目的数量。这使得拖动非常起伏不定,并且会随着列表项的增加而变得越来越起伏不定。
知道为什么 React.memo 对我的情况不起作用吗?
编辑:我发现部分原因在于,当拖放多个列表项时,悬停代码不再正确计算。并没有消除这样一个事实,即它不需要在仅拖动几个列表项时重新呈现所有列表项。
我不确定为什么尽管使用了 React.memo,所有项目仍然会重新渲染,但我通过修复以下代码摆脱了断断续续的情况:
const tabSource = {
beginDrag({ selectedTabsState, windowId, tabIndex }) {
return { windowId, tabIndex, selectedTabs: selectedTabsState.selectedTabs };
}
};
这导致拖动悬停计算触发过多的状态更新,随后在 redux 更新状态时大量重新渲染。
由于重新渲染太多,我的拖放速度很慢。
React.memo
似乎没有帮助,尽管我将所有项目都作为基元传递。
我的列表如下所示:
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);
我的列表项如下所示:
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);
代码一次只拖动选定的项目(这必须在自定义拖动层中显示);它不会抛出异常(它有效),但它非常慢。
在我的控制台中,我可以看到该项目被渲染了 48 次,这是我拥有的列表项目的数量。这使得拖动非常起伏不定,并且会随着列表项的增加而变得越来越起伏不定。
知道为什么 React.memo 对我的情况不起作用吗?
编辑:我发现部分原因在于,当拖放多个列表项时,悬停代码不再正确计算。并没有消除这样一个事实,即它不需要在仅拖动几个列表项时重新呈现所有列表项。
我不确定为什么尽管使用了 React.memo,所有项目仍然会重新渲染,但我通过修复以下代码摆脱了断断续续的情况:
const tabSource = {
beginDrag({ selectedTabsState, windowId, tabIndex }) {
return { windowId, tabIndex, selectedTabs: selectedTabsState.selectedTabs };
}
};
这导致拖动悬停计算触发过多的状态更新,随后在 redux 更新状态时大量重新渲染。