react-beautiful-dnd draggable flickering (but only some of the draggables children) - reactjs

I am having trouble getting react-beautiful-dnd to work because children of the draggable are flickering after reordering:
No external requests happen in the background, the app stores the change only in the local state (see first code block reorderElements()).
What really confuses me is how only the 2 checkboxes flicker, but not the text or the icon.
The list rerenders only once after reorder (asynchronously when the state is updated)
I suspect the asynchronous nature of useState to be the issue. But the fact that only those 2 checkboxes flicker contradicts the suspicion in my opinion.
The draggableId is updated after each reorder to the index of each element.
Here is the code:
My reorder function that is called onDragEnd:
function reorderElements({ source, destination }: any) {
if (!destination) {
return;
}
setAppState((oldAppState: AppState | null) => {
if (oldAppState) {
const { content } = oldAppState;
const copy = JSON.parse(JSON.stringify(content));
const [elementToMove] = copy.splice(source.index, 1);
copy.splice(destination.index, 0, elementToMove);
const elementsWithNewIndex = copy.map(
(e: BannerElement, i: number) => ({
...e,
id: "" + i,
})
);
return { ...oldAppState, content: elementsWithNewIndex };
}
return null;
});
}
The list component
<DragDropContextContainer onDragEnd={onDragEnd}>
<DroppableContainer direction="horizontal" droppableId="root">
{(provided: DroppableProvided) => (
<div
style={{ display: "flex" }}
ref={provided.innerRef}
{...provided.droppableProps}
className="draggable-container-custom"
>
<>
{content.map((element, index) => (
<Element
key={index}
index={index}
element={element}
updateElement={updateElement}
deleteElement={deleteElement}
provided={provided}
nElements={nElements}
/>
))}
{provided.placeholder}
</>
</div>
)}
</DroppableContainer>
</DragDropContextContainer>
DragDropContextContainer
export function DragDropContextContainer({ children, ...props }: Props) {
return <DragDropContext {...props}>{children}</DragDropContext>;
}
DroppableContainer
export function DroppableContainer({ children, ...props }: Props) {
return <Droppable {...props}>{children}</Droppable>;
}
Each Element
<DraggableContainer draggableId={id} index={index} key={index}>
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
const draggableStyle = snapshot.isDragging
? { ...customDraggableStyle, ...provided.draggableProps.style }
: provided.draggableProps.style;
return (
<div
ref={provided.innerRef}
{...provided.draggableProps}
style={draggableStyle}
className="draggable-custom"
>
<div
{...provided.dragHandleProps}
className="element-drag-icon-container"
>
<div className="element-drag-icon">
<PolarisIcon source={DragHandleMinor} color={"base"} />
</div>
</div>
<div className="element-container">
<div className="element-icon-container">
<Checkbox
label="Text has Icon Prefix"
checked={hasIcon}
onChange={(newValue) => {
updateElement({ id, hasIcon: newValue });
}}
/>
<Icon
disabled={!hasIcon}
key={JSON.stringify(icon)}
id={id}
elementIcon={icon}
updateElement={updateElement}
/>
</div>
<div className="element-text-container">
<TextField
label="Set your Text:"
multiline={3}
value={text}
onChange={(text: ElementText) => {
updateElement({ id, text });
}}
autoComplete="off"
/>
</div>
<div className="element-bottom-row">
<Checkbox
label="Show on mobile"
checked={showMobile}
onChange={() => {
updateElement({ id, showMobile: true });
}}
/>
<Button
plain
destructive
onClick={() => {
deleteElement(id);
}}
>
delete
</Button>
</div>
</div>
</div>
);
}}
</DraggableContainer>
);
DraggableContainer
export function DraggableContainer({ children, ...props }: Props) {
return <Draggable {...props}>{children}</Draggable>;
}
Those container-Components exist to get rid of some ESLint errors.
Console logs nothing regarding this package.
im using "react": "^17.0.2" and "react-beautiful-dnd": "^13.1.0"
tested in Chrome Version 108.0.5359.124 (Official Build) (arm64)

Related

DnD-Kit Incorrect behavior while trying to move item to another container in ReactJs

I would appreciate any help with this case, so if you see any minor issue - please, write me. There will be rather a lot of code.
I was trying to implement 'dnd-kit/sortable' into my bug tracker app. I have Kanban board consisting of four repeating column components. I needed to implement dnd-kit to be able to move task cards not only inside of each column, but between columns as well. Current code with sorting task cards in column, but if you try to move a card to any other column - most of the time nothing happens, but sometimes you get the Uncaught TypeError: Cannot read properties of undefined (reading 'id') I red through documentation many times and looked through similar projects in open source, but couldn't find what could be the reason for this bug.
The tasks from TasksContext is object with keys backlog, todo, inProgress, inReview, done and contains array of object. Each object inside of array represents task card.
Dashboard.js
const Dashboard = () => {
const { tasks, setTasks } = useContext(TasksContext)
const [activeId, setActiveId] = useState(null);
const mouseSensor = useSensor(MouseSensor);
const touchSensor = useSensor(TouchSensor);
const sensors = useSensors(mouseSensor, touchSensor)
const fullArray = Array.from(Object.values(tasks).flat())
console.log(fullArray)
const handleDragStart = ({ active }) => setActiveId(active.id);
const handleDragCancel = () => setActiveId(null);
const handleDragEnd = ({active, over}) => {
const { containerId: activeContainer } = active.data.current.sortable
const { containerId: overContainer } = over.data.current.sortable
const oldIndex = tasks[activeContainer].findIndex(obj => obj.id === active.id);
const newIndex = tasks[overContainer].findIndex(obj => obj.id === over.id);
if (active.id !== over.id) {
setTasks((prevTasks) => ({
...prevTasks,
[overContainer]: arrayMove(prevTasks[overContainer], oldIndex, newIndex)
}));
}
setActiveId(null);
}
return (
<div className='relative grid grid-cols-4 gap-6 px-6 grow-0 shrink-0 basis-5/6 overflow-y-scroll'>
<DndContext sensors={sensors} collisionDetection={rectIntersection} onDragStart={handleDragStart} onDragCancel={handleDragCancel} onDragEnd={handleDragEnd}>
<TasksColumn key='to do' title='to do' id='todo' tasks={tasks.todo} />
<TasksColumn key='in progress' title='in progress' id='inProgress' tasks={tasks.inProgress} />
<TasksColumn key='in review' title='in review' id='inReview' tasks={tasks.inReview} />
<TasksColumn key='done' title='done' id='done' tasks={tasks.done} />
<DragOverlay>{activeId ? <TaskCard id={activeId} task={fullArray.filter(task => task?.id === activeId)[0]} /> : null}</DragOverlay>
</DndContext>
</div>
)
}
TasksColumn.js
const TasksColumn = ({ title, id, tasks }) => {
const { setNodeRef } = useDroppable({id});
return (
<div className=''>
<ColumnHeader title={title} id={id} />
<div className="h-3 w-full border-b-2 border-grayDark" />
<SortableContext items={tasks} id={id} strategy={verticalListSortingStrategy}>
<div ref={setNodeRef} className=''>
{tasks.map(task => (
<Draggable key={task.name} id={task.id} task={task} />
))}
</div>
</SortableContext>
</div>
)
}
Draggable.js
const Draggable = ({ id, task }) => {
const { setNodeRef, transform, transition, isDragging, } = useSortable({id});
const style = {
transform: CSS.Translate.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div ref={setNodeRef} style={style}>
<TaskCard id={id} task={task} />
</div>
)
}
TaskCard.js
const TaskCard = ({ id, task }) => {
const { attributes, listeners, setActivatorNodeRef } = useSortable({id});
return (
<div className="py-4 border-b-2 border-grayLight">
<div className="">
<p className="">{task.deadline}</p>
<p className="">{task.priority}</p>
</div>
<ArrowsPointingOutIcon className='rotate-45 w-5 h-5 outline-none' ref={setActivatorNodeRef} {...listeners} {...attributes} />
<p className="">{task.name}</p>
<div className="">
<p className="">{task.author}</p>
<p className="">{task.time}</p>
</div>
</div>
)
}

How to set hover in a React loop and effect only one instead of all elements in the loop?

When I use setHover it reflects to all list data which returned from map loop. How can I use hover to reflect on itself element?
const [hover, setHover] = useState(true)
function MouseOver(event) {
setHover(true)
}
function MouseOut(event){
setHover(false)
}
{data.map((item, index) => (
//When I hover parent div I want to show the {item.arrow} div inside and not all {item.arrow} divs in the loop
<div key={index} onMouseEnter={MouseOver} onMouseLeave={MouseOut} className="flex gap-3">
<div>
{item.content}
</div>
<div hidden={hover}>
{item.arrow}
</div>
</div>
))}
If the state does not need to be controlled by the parent you can create a new component to use in the list.
Each component will then control its own hover state.
const List = ({data}) => {
return (
<div>
{
data.map((item, index) => (<Item key={index} item={item} />))
}
</div>
)
}
const Item = ({item}) => {
const [hover, setHover] = useState(true)
const mouseOver = (event) => {
setHover(true)
}
const mouseOut = (event) => {
setHover(false)
}
return (
<div onMouseEnter={mouseOver} onMouseLeave={mouseOut} className="flex gap-3">
<div>
{item.content}
</div>
<div hidden={hover}>
{item.arrow}
</div>
</div>
);
}
If the state does need to be controlled by the parent you can use a Record<number, boolean> to store the states.
const List = ({data}) => {
const [hover, setHover] = useState({})
const mouseOver = (event, index) => {
setHover(c => {
return {
...c,
[index]: true
};
})
}
const mouseOut = (event, index) => {
setHover(c => {
return {
...c,
[index]: false
};
})
}
return (
<div>
{
data.map((item, index) => (
<div
key={index}
onMouseEnter={(e) => {
mouseOver(e, index);
}}
onMouseLeave={(e) => {
mouseOut(e, index);
}}
className="flex gap-3"
>
<div>
{item.content}
</div>
<div hidden={hover[index]}>
{item.arrow}
</div>
</div>
))
}
</div>
)
}
If the state is not needed for anything other than hiding a div you could also just use CSS.
CSS will not require the component to rerender everytime you hover over it.
CSS
.hoverable-show {
display: none;
}
.hoverable-item:hover .hoverable-show {
display: block;
}
JS
const List = ({data}) => {
return (
<div>
{
data.map((item, index) => (
<div
className="flex gap-3 hoverable-item"
>
<div>
{item.content}
</div>
<div className="hoverable-show">
{item.arrow}
</div>
</div>
))
}
</div>
)
}
Preference should be CSS -> Individual State -> Parent (list) State.
This looks like a use case for the useReducer hook available right from the react library.

React: Muuri-react generate random items error - Uncaught Invariant: Invariant failed: The item has not been setted yet

The muuri-react demonstrates to generate and add random items.
There was a small modification on the codes to randomly generate 3 items rather than adding 3 more random items. When I modified in codepen and ran it, the items could render successfully. However, when I tried it in a local machine, it couldn't.
When I clicked the "Generate item" button, nothing was appearing on the div, and after the second click, an error message "Uncaught Invariant: Invariant failed: The item has not been setted yet at invariant" shown on the console.
Could anyone tell me what goes wrong?
The following is the code:
import { useFilter, generateItems, options } from "./utils";
import { MuuriComponent } from "muuri-react";
import './style.css'
function MuuriDemo() {
const [items, setItems] = useState(generateItems());
const Item = ({ color, width, height, title, remove }) => {
console.log(color);
return (
<div className={`item h${height} w${width} ${color}`}>
<div className="item-content">
<div className="card">
<div className="card-title">{title}</div>
<div className="card-remove">
<i className="material-icons" onMouseDown={remove}>

</i>
</div>
</div>
</div>
</div>
);
};
// Children.
const children = items.map(({ id, color, title, width, height }) => (
<Item
key={id}
color={color}
title={title}
width={width}
height={height}
/>
));
return (
<div>
<button onClick={() => setItems(generateItems())}>Generate item</button>
<section className="grid-demo">
<MuuriComponent
{...options}
propsToData={({ color, title }) => ({ color, title })}
>
{children}
</MuuriComponent>
</section>
</div>
)
}
export default MuuriDemo```
I have managed to get it works via the following code. I have no idea whether it is a standard way.
import { generateItems, options } from "./utils";
import { MuuriComponent } from "muuri-react";
import './style.css'
import { render } from "react-dom";
function MuuriDemo() {
const [items, setItems] = useState(generateItems());
const Item = ({ color, width, height, title, remove }) => {
console.log(color);
return (
<div className={`item h${height} w${width} ${color}`}>
<div className="item-content">
<div className="card">
<div className="card-title">{title}</div>
<div className="card-remove">
<i className="material-icons" onMouseDown={remove}>

</i>
</div>
</div>
</div>
</div>
);
};
// Children.
const children = items.map(({ id, color, title, width, height }) => (
<Item
key={id}
color={color}
title={title}
width={width}
height={height}
/>
));
useEffect(() => {
render(<div>
<button onClick={() => setItems(generateItems())}>Generate item</button>
<section className="grid-demo">
<MuuriComponent
{...options}
propsToData={({ color, title }) => ({ color, title })}
>
{children}
</MuuriComponent>
</section></div>, document.getElementById('muuri'))
console.log(children);
}, [children])
return (
<div id='muuri'/>
)
}
export default MuuriDemo

React-virtualized list running below 60fps. How to optimize?

I am trying to create a post feed like the one instagram has (on the main page).
I'm using Infinite-loader for fetching, Window-scroller for using the window as the scroll, auto-sizer for sizing the list how i want and CellMeasurer for measuring the 'post component' once after the image has been loaded.
Here is code for the list component:
class PostsPartial extends React.PureComponent<IProps>{
state: IPostsPartialState = { posts: [], hasMorePosts: true }
private cache: CellMeasurerCache;
private get rowCount(): number {
return this.state.hasMorePosts ? this.state.posts.length + 1 : this.state.posts.length;
}
constructor(props: IProps) {
super(props);
this.cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 1000
});
this.renderRow = this.renderRow.bind(this);
}
private fetchPosts = ({ startIndex, stopIndex }: { startIndex: number, stopIndex: number }) => {
return getNewPostsChunk(startIndex, stopIndex - startIndex, this.props.token).then((res: IPostsChunkResponse) => {
if (res.success) {
if (res.posts.length === 0) {
// no more posts
this.setState({ hasMorePosts: false })
}
else {
let newPosts = [...this.state.posts, ...res.posts];
this.setState({ posts: newPosts })
}
}
else {
// internal error
}
})
};
private renderRow({ index, key, parent, style }: any) {
return (
<CellMeasurer
cache={this.cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
{({ measure, registerChild }: any) => (
<div className={styles.paddingContainer} ref={registerChild} style={style}>
<Post
isLoaded={this.isRowLoaded({index})}
measure={measure}
post={this.state.posts[index]}
/>
</div>
)}
</CellMeasurer>
);
}
private isRowLoaded = ({ index }: { index: number }) => {
return !!this.state.posts[index];
};
public render() {
return (
<div className={styles.mainContainer}>
<InfiniteLoader
isRowLoaded={this.isRowLoaded}
loadMoreRows={this.fetchPosts}
rowCount={this.rowCount}
>
{({ onRowsRendered, registerChild }: InfiniteLoaderChildProps) => (
<WindowScroller>
{({ height, isScrolling, onChildScroll, scrollTop }) => (
<AutoSizer disableHeight>
{
({ width }: any) => (
<List
ref={registerChild}
onRowsRendered={onRowsRendered}
autoHeight
width={width}
height={height}
isScrolling={isScrolling}
onScroll={onChildScroll}
scrollTop={scrollTop}
deferredMeasurementCache={this.cache}
rowHeight={this.cache.rowHeight}
rowRenderer={this.renderRow}
rowCount={this.rowCount}
overscanRowCount={10}
/>
)
}
</AutoSizer>
)}
</WindowScroller>
)}
</InfiniteLoader>
</div>
);
}
and here is code for the post component:
const Post:React.FC<IProps> = (props:IProps) => {
if(props.post && props.isLoaded)
return (
<div className={styles.container}>
<Segment className={styles.profileSegmentInternal} attached='top'>
<Image className={styles.verySmallImg} circular size='tiny' src={`${settings.BASE_URL}/feed/photo/user/${props.post.creator}`}></Image>
<Link to={`/profile/${props.post.creator}`}>
<Header size='small' className={styles.headerName} as='span'>{props.post.creator}</Header>
</Link>
</Segment>
<div className={styles.imageContainer}>
<Image onLoad={props.measure} src={`${settings.BASE_URL}/feed/photo/post/${props.post._id}`} className={styles.image}></Image>
</div>
<Segment className={styles.bottomSegment} attached='bottom'>
<>
<Menu className={styles.postMenu}>
<Item className='left'>
<Icon className={styles.iconBtn} size='big' name='heart outline'></Icon>
<Icon className={styles.iconBtn} size='big' name='comment outline'></Icon>
<Icon className={styles.iconBtn} size='big' name='paper plane outline'></Icon>
</Item>
<Item className='right'>
<Icon className={styles.iconBtn} size='big' name='bookmark outline'></Icon>
</Item>
</Menu>
</>
<Header className={styles.likes} size='tiny'>{props.post.likesCount} likes</Header>
<Header className={styles.description} size='tiny'>
<Header size='tiny' className={styles.commentUsername} as='span'>{props.post.creator}</Header>
<Header className={styles.commentText} as='span' size='tiny'> {props.post.description}</Header>
</Header>
<Link to='#'>
<Header className={styles.viewAllComments} size='tiny' disabled>View all comments</Header>
</Link>
{
//backend will return the first 3-4 messeges only
// props.post.messeges.map((messege,index) => (
// ))
}
<Form className={styles.commentForm}>
<Form.Field className={styles.commentField}>
<Form.Input
className={styles.commentInput}
placeholder='Adding comment ...'
>
</Form.Input>
<Button className={styles.commentSubmit} size='medium' primary>Comment</Button>
</Form.Field>
</Form>
</Segment>
</div>
)
else
return (
<p>loading</p>
)
Even if I remove everything from the post component and leave only the image, it still won't run with more then 45-50fps sometimes going under 40fps too.
Can I optimize my approach in any way or am I doing something wrong?
Should I provide anything else that might be helpful?
Thank you in advance!
So I fixed my problem by resizing the image when uploading it (in the backend using sharp).
This way the fetching is faster witch makes the (just in time) measure of a post component way faster as less data needs to be loaded on mount, and html+css doesn't have to resize the high image into a smaller container.
Sounds silly, but I didn't think of this being the issue and instead focused on my infinite scrolling implementation :D
Ya live and ya learn
EDIT:
I forgot to mention, a small change I did when setting the image src.
Instead of making an express route that retrieves the src, I can just use it while receiving the post info. The file source won't be printed with console.log or whatever, but it is there and can be used like so:
<Image className={styles.image} onLoad={props.measure} src={`data:${props.post.source.contentType};base64,${Buffer.from(props.post.source.data).toString('base64')}`} />

React Beautiful Dnd : after dropping the item, source and destination indices are undefined

I'm new to react.js and trying to create a basic Drag and Drop using
react-beautiful-dnd. I'm unable to find te issue after multiple tries, below is the code, drag and drop is fine but after dropping list is not reordering basically, in onDragEnd function result.source.index and result.destination.index is undefined.
import React from 'react';
import ReactDOM from 'react-dom'
import {
DragDropContext, Droppable, Draggable
} from 'react-beautiful-dnd'
//data
const getItems = (count) => Array.from({length: count}, (v, k) => k).map(k => ({
id: `item-${k}`,
content: `item ${k}`
}))
/** Reorder an array, moving the item at $startIndex to $endIndex. */
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list)
const [removed] = result.splice(startIndex, 1)
result.splice(endIndex, 0, removed)
return result
}
// inline style
const grid = 8
const getItemStyle = (dragabbleStyle, isDragging) => ({
userSelect: 'none',
padding: grid * 2,
marginBotom: grid,
background: isDragging ? 'lightgreen': 'grey',
...dragabbleStyle
})
const getListStyle = (isDraggingOver) => ({
background: isDraggingOver ? 'lightblue' : 'lightgrey',
padding: grid,
width: 250
})
class AppDnD extends React.Component {
constructor(props) {
super(props)
this.state = {
items: getItems(10)
}
}
onDragEnd = (result) => {
if(!result.destination) {return}
console.log(result)
const items = reorder (
this.state.items,
result.source.index,
result.destination.index,
)
this.setState({items})
}
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<Droppable droppableId="droppable">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
>
{this.state.items.map(item => (
<Draggable
key={item.id}
draggableId={item.id}
>
{(provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
style={
getItemStyle(
provided.draggableProps.style,
snapshot.isDragging
)}
{...provided.dragHandleProps}
{...provided.draggableProps}
>
{item.content}
</div>
{provided.placeholder}
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
)
}
}
export default AppDnD;
Could someone give me a breakdown how drag and drop works in react-beautiful-dnd. My actual target is to do a drag and drop behaviour for a Table, it would be great if someone provide some points for it.
I've found the solution, in <Draggable> component we need to define id={i} attribute. The whole statement would look like this `
Droppable id needs an index which is changing
array.map((items, index) => (
<Droppable droppableId={index.toString()}>
{(provided) => (
<div className="calendar-col" {...provided.droppableProps} ref={provided.innerRef} key={index}>
<div className="calendar-date-cell">
<div className="date-content">
{items[0]?.startDate}
</div>
</div>
</div>
)}
</Droppable>
))

Resources