I'm working with a Material UI stepper wherein each step returns a list of selected items.
In each step, the user can click an item that is highlighted before moving onto the next step.
The results are mapped in a separate component.
const [clicked, setClicked] = useState(undefined);
const selectStyleAndFilter = (item, index) => {
setClicked(index);
selectedItems[activeStep] = item;
};
const handleBack = () => {
setActiveStep(activeStep - 1);
setClicked(undefined);
if (activeStep === 1) {
nextStep(selectedItems).then(products => {
console.log(
'productCount back',
products.data.data.contents.total
);
setActiveStep(activeStep - 1);
setClicked(undefined);
setProductCount(
products.data.data.contents.total === undefined
);
});
}
};
However, when I click the back button to return to the previous step, the item is no longer highlighted.
I tried using the logic from selectStyleAndFilter in the handleBack function to keep the item in state:
const handleBack = (item, index) => {
setActiveStep(activeStep - 1);
setClicked(index);
selectedItems[activeStep] = item;
How can I keep the setClicked state of the previous item applied in the handleBack function?
Convert clicked to be an object that holds the clicked item for each activeStep.
const [clicked, setClicked] = useState({});
The current clicked is derived from the clicked object and activeState
const currentClicked = clicked[activeStep];
When you set the currently clicked item, store it under the activeSet, and maintain the state of the other clicked items:
const selectStyleAndFilter = (item, index) => {
// add the current, but maintain the state of the others
setClicked(c => ({ ...c, [activeStep]: index }));
selectedItems[activeStep] = item;
};
Don't reset the clicked state when moving between steps:
const handleBack = () => {
setActiveStep(activeStep - 1);
if (activeStep === 1) {
nextStep(selectedItems).then(products => {
console.log(
'productCount back',
products.data.data.contents.total
);
setActiveStep(activeStep - 1);
setProductCount(
products.data.data.contents.total === undefined
);
});
}
};
Related
Im trying to make a calendar widget where you can click and create an event at a certain starttime, if you hold mouse down and move down, the event becomes bigger (endtime gets later). Which works in the frontend, but i'm not able to acces the event i just created in my events state.
I useState for const array of events. On mouseDown i create an event and update the events' state. Which works. On mouseMove i update the events state again by finding the event for it's key in the prevState of the events array. Which also works.
But on mouseUp i want to find the event for it's key again to return the object i just created. But my events state does not contain the event i created on the screen. This time i just want to use the already created eventObject in another function, not update it. Howcome this event is not available in my events state on mouseUp, while it was already available in my mouseMove part. It probably has someting todo with that i'm accessing events from prevState in the mousemove and im not doing that in mouseUp. But how do you acces the most recent state when you don't want to update the state at that moment? (see comments added to code).
const [key, setKey] = useState(1000);
// creating state of array of events here.
const [events, setEvents] = useState(null)
const handleClick = (e) => {
console.log("HandleClick...");
const eventsSameDateTime = events.filter(
ev => {
const sameGridRow = ev.gridRowStart === getRowForEvent(e);
const sameGridColum = ev.gridcolumn === getColumnForEvent(e);;
return sameGridRow && sameGridColum}
);
const count = eventsSameDateTime.length+1;
newEvent = {
date: null,
gridRowStart: getRowForEvent(e),
gridRowEnd: getRowForEvent(e)+4,
gridcolumn: getColumnForEvent(e),
title: "new",
createDate: new Date(),
zIndex: count,
};
const newEvents = [...eventsSameDateTime, newEvent]
.sort((a, b) => a.createDate.getTime() - b.createDate.getTime())
.map((ev, i) => {
const newKey = key+i;
ev.key = newKey;
setKey(prevState => prevState+i)
ev.zIndex = i + 1;
ev.otherEvents = count;
return ev;
})
// updating the events state here onclick (initial creation)
setEvents((prevState) => {
const oldEvents = prevState.filter(pEv => newEvents.find(nEv => nEv.key === pEv.key) == null);
return [...oldEvents, ...newEvents];
})
}
const handleMouseDown = (e1) => {
console.log("HandleMouseDown...");
if (e1.target.className.includes("event-container")) {
setKey(prevState => {
console.log(`updating key onClick...`);
return prevState+1;
});
e1.preventDefault();
handleClick(e1);
const handleMouseMove = (e2) => {
console.log("HandleMouseMove...")
e2.preventDefault();
// updating the events array state on mousemove.(one event endstime in the array)
setEvents((prevState) => {
console.log(`updating events onMove...`);
// the event i created on click is available in the prevState.
console.log("events on mouseMove: ", prevState);
return [...prevState].map((ev) => {
if (ev.key === newEvent.key) {
ev.gridRowEnd = getRowForEvent(e2)
}
return ev;
})
})
}
e1.target.addEventListener('mousemove', handleMouseMove);
const handleMouseUp = (e3) => {
e3.preventDefault();
console.log("HandleMouseUP");
const removeMouseMoveAndDown = (e) => {
e.removeEventListener("mousemove", handleMouseMove);
e.removeEventListener("mouseup", handleMouseUp);
}
if (isEvent(e1.target)) {
console.log("Removing Event mouseUpDown");
removeMouseMoveAndDown(e1.target);
}
const eventContainer = findWeekElement(e1.target);
console.log("Removing EventContainer mouseUpDown");
removeMouseMoveAndDown(eventContainer);
// when i log the events array, the event i created onclick is not in there.
console.log("events on mouseUp: ", events);
const existingEvent = events.find(ev => ev.index === key);
props.payload.setValue(JSON.stringify(createUiEventForEvent(existingEvent)));
props.eventActions.onCreate();
}
e1.target.addEventListener("mouseup", handleMouseUp);
} else {
console.log("nothing happens, you probably clicked an event");
}
}
I am making a card system that can swap cards using drag and drop. When I'm dragging the cards over each other my state is updating as it's supposed to but it's not re-rendering.
import React, {useRef, useState} from 'react';
import DnDGroup from './DndGroup';
const DragNDrop = ({data}) => {
const [list, setList] = useState(data);
const dragItem = useRef();
const dragNode = useRef();
const getParams = (element) => {
const itemNum = element.toString();
const group = list.find(group => group.items.find(item => item === itemNum))
const groupIndex = list.indexOf(group);
const itemIndex = group.items.indexOf(itemNum);
return {'groupIndex': groupIndex, 'itemIndex': itemIndex}
}
const handleDragstart = (e) => {
dragNode.current = e.target;
setTimeout(() => {
dragNode.current.classList.add('current')
}, 0)
dragItem.current = getParams(e.target.value);
dragNode.current.addEventListener('dragend', () => {
if(dragNode.current !== undefined) {
dragNode.current.classList.remove('current')
}
})
}
const handleDragEnter = e => {
if(dragNode.current !== e.target && e.target.value !== undefined) {
const node = getParams(e.target.value);
const currentItem = dragItem.current;
[data[currentItem.groupIndex].items[currentItem.itemIndex], data[node.groupIndex].items[node.itemIndex]] = [data[node.groupIndex].items[node.itemIndex], data[currentItem.groupIndex].items[currentItem.itemIndex]];
setList(data);
console.log(list)
}
}
return (
<div className='drag-n-drop'>
{list.map(group => (
<DnDGroup
key={group.title}
group={group}
handleDragstart={handleDragstart}
handleDragEnter={handleDragEnter}
/>
))}
</div>
);
};
export default DragNDrop;
I also tried to do this:
setList([...data])
Using this it renders according to the state changes and works great inside one group, but when I want to drag a card to the other group, the state constantly changes back and forth like crazy, it also gives tons of console.logs.
How can I fix this?
With this line:
[data[currentItem.groupIndex].items[currentItem.itemIndex], data[node.groupIndex].items[node.itemIndex]] = [data[node.groupIndex].items[node.itemIndex], data[currentItem.groupIndex].items[currentItem.itemIndex]];
you're mutating a deeply nested object inside the data array. Never mutate state in React; it can result in components not re-rendering when you expect them to, in addition to other unexpected (and undesirable) side-effects.
Another thing that looks like it's probably an unintentional bug is that you're changing the data (original state) instead of the stateful list array.
Change it to:
const itemA = list[node.groupIndex].items[node.itemIndex];
const itemB = list[currentItem.groupIndex].items[currentItem.itemIndex];
const newList = [...list];
newList[currentItem.groupIndex] = {
...newList[currentItem.groupIndex],
items: newList[currentItem.groupIndex].map(
(item, i) => i !== currentItem.itemIndex ? item : itemB
)
};
newList[node.groupIndex] = {
...newList[node.groupIndex],
items: newList[node.groupIndex].map(
(item, i) => i !== node.itemIndex ? item : itemA
)
};
setList(newList);
This avoids the mutation of the nested items subarray.
Below code of incrementing the button. But am facing the issue in the increment function to increment the value.
const [items, setItems] = useState([
{itemName:'item1', quantity:2, isSelected:false},
{itemName:'item2', quantity:5, isSelected:false},
{itemName:'item3', quantity:7, isSelected:false}
]);
const increment = (index) => {
setItems([...items,
index.quantity++
]) }
<button onClick={() => increment(index)}>increment</button>
You are adding an item to the items array instead of editing the desired item, remember that you should also treat state immutably:
const increment = (index) => {
setItems((items) => {
const prevItem = items[index];
return Object.assign([], items, {
[index]: { ...prevItem, quantity: prevItem.quantity + 1 },
});
});
};
I'm using the grid api to get the selected rows by using the function getSelectedRows(). However, the list of rows seems to be unordered, i.e the items are not in the order I selected them in.
Is there another way to get the selected rows in the order they were selected?
You can keep track of the selected items yourself and make sure it's chronologically ordered by listening to selectionChanged event.
// global keyboard state, put this outside of the function body
// we need to store the current shift key state to know if user
// are selecting multiple rows
const KEYBOARD_STATE = {
isShiftPressed: false
};
document.addEventListener("keydown", (e) => {
KEYBOARD_STATE.isShiftPressed = e.key === "Shift";
});
document.addEventListener("keyup", (e) => {
KEYBOARD_STATE.isShiftPressed = false;
});
const [selection, setSelection] = React.useState([]);
const onSelectionChanged = (e) => {
const selectedNodes = e.api.getSelectedNodes();
const lastSelectedNode =
selectedNodes[0]?.selectionController?.lastSelectedNode;
// if lastSelectedNode is missing while multi-selecting,
// AgGrid will select from the first row
const selectedNodeFrom = lastSelectedNode || e.api.getRenderedNodes()[0];
const isRangeSelect = KEYBOARD_STATE.isShiftPressed;
const difference = (arr1, arr2) => arr1.filter((x) => !arr2.includes(x));
const newSelection = difference(selectedNodes, selection);
if (newSelection.length > 0) {
if (isRangeSelect) {
const isSelectBackward =
newSelection[0].rowIndex < selectedNodeFrom.rowIndex;
if (isSelectBackward) {
newSelection.reverse();
}
}
newSelection.forEach((n) => updateSelection(n));
} else {
updateSelection(); // deselect
}
};
const updateSelection = (rowNode) => {
setSelection((selections) => {
if (rowNode) {
const isSelected = rowNode.isSelected();
if (isSelected) {
return [...selections, rowNode];
} else {
return selections.filter((s) => s.id !== rowNode.id);
}
} else {
return selections.filter((n) => n.isSelected());
}
});
};
return (
<>
<pre>
{JSON.stringify(selection.map((n) => n.data.id))}
</pre>
<AgGridReact
rowSelection="multiple"
columnDefs={columnDefs}
rowMultiSelectWithClick
onSelectionChanged={onSelectionChanged}
{...}
/>
</>
);
Live Demo
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.