react-beautiful-dnd pull out the anonymous function from React Draggable component - reactjs

I need to pull out the anonymous function from Draggable into a callback function using the useCallback(..., []) hook supplying the dependencies provided, snapshot if possible.
Upon investigation we have found that the 3rd party component Draggable is using an anonymous function that essentially recreates the function on the stack for each step in the list. When this happens for a route with 500 steps the stack trace gets increased by 500 bringing us closer to a stack overflow.
React has a virtual dom, and if the child component has not changed it will not re-render it. But anonymous functions make it re-render every time, but if the anonymous function is pushed down one more layer it won't see it as changed.
Here is my implemented code.
const StepContainer = ({
step,
index,
draggedDescription,
isSelected,
selectedTaskIdsCount
}: Props) => {
return (
<Box data-testid="stepContainer" className={stepContainer}>
<Box className={'hoverstyle'}>
<Draggable draggableId={step.key} index={index} key={step.key}>
{(
provided: DraggableProvided,
snapshot: DraggableStateSnapshot
) => (
<div
data-testid="clickHandler"
{...provided.draggableProps}
ref={provided.innerRef}
data-is-dragging={
snapshot.isDragging && !snapshot.isDropAnimating
}
className={snapshot.isDragging ? `${dragging}` : ''}
>
{snapshot.isDragging ? (
<DraggedStepAsset
draggedDescription={draggedDescription}
stepCount={
selectedTaskIdsCount > 1
? selectedTaskIdsCount
: 1
}
/>
) : (
<AccordionSteps
isSelected={isSelected}
provided={provided}
step={step}
/>
)}
</div>
)}
</Draggable>
</Box>
</Box>
);
};
The idea is to pull out the anonymous function into a callback function using the useCallback(..., []) hook supplying the dependecies provided, snapshot if possible. Then we can wrap the useCallback function with a useMemo() to ensure React memoizes it, this could potentially enable react to reuse the same function on the stack for every step-component instead of adding it for each step-component. Making our code much more optimized.
Please help me to resolve this issue.

Hope the below snippet works for you.
const StepContainer = ({
step,
index,
draggedDescription,
isSelected,
selectedTaskIdsCount
}: Props) => {
return (
<Box data-testid="stepContainer" className={stepContainer}>
<Box className={'hoverstyle'}>
<Draggable draggableId={step.key} index={index} key={step.key}>
{DraggableContentCallback}
</Draggable>
</Box>
</Box>
);
};
const DraggableContent = (
provided: DraggableProvided,
snapshot: DraggableStateSnapshot
) => (
<div
data-testid="clickHandler"
{...provided.draggableProps}
ref={provided.innerRef}
data-is-dragging={
snapshot.isDragging && !snapshot.isDropAnimating
}
className={snapshot.isDragging ? `${dragging}` : ''}
>
{snapshot.isDragging ? (
<DraggedStepAsset
draggedDescription={draggedDescription}
stepCount={
selectedTaskIdsCount > 1
? selectedTaskIdsCount
: 1
}
/>
) : (
<AccordionSteps
isSelected={isSelected}
provided={provided}
step={step}
/>
)}
</div>
);
const DraggableContentCallback = useCallback(DraggableContent, []);

Related

“react-infinite-scroll-component” Stopped working after one call (loadMore only gets called once)

I use the react-infinite-scroll-component library for pagination,but even though hasMore is true, loadMore is called once.
<InfiniteScroll
dataLength={100}
pullDownToRefreshThreshold={50}
next={loadMoreConversation}
scrollableTarget="scrollableDiv"
hasMore={true}
loader={<h4>Loading...</h4>}
>
<ChatItemList
chatItems={chatItems}
isInDeleteMode={deleteActive}
onBottomDrawerHandler={onBottomDrawerHandler}
/>
</InfiniteScroll>
Please help me with this, What am I doing wrong?
I had this issue. It turns out I was using dataLength in the wrong way. I thought it was supposed to be the total length of items that could be displayed, but instead, it seems it should be the length of items that are currently displayed, so the correct way is should be something like this:
<InfiniteScroll
dataLength={page * 10}
pullDownToRefreshThreshold={50}
next={loadMoreConversation}
scrollableTarget="scrollableDiv"
hasMore={true}
loader={<h4>Loading...</h4>}
>
<ChatItemList
chatItems={chatItems}
isInDeleteMode={deleteActive}
onBottomDrawerHandler={onBottomDrawerHandler}
/>
</InfiniteScroll>
in this case, I load 10 items per page. I hope this helps you because I searched a lot and did not find an answer until I reached it with all the effort and error.
This is a React Query useInfiniteQuery() example, assuming you have:
data: {
pages: [
{/*a page*/
results:[
{/*an item*/
/*...*/
}
]
}
]
}
Set your dataLength this way:
const {isSuccess,
isLoading,
isError,
data,
error,
hasNextPage,
fetchNextPage,
isFetchingNextPage
} = useInfiniteQuery(
path /*your query path here*/,
fetchFn /*your fetch function here*/);
return <div>
{isError && <div>Error! {error?.message}</div>}
{isLoading && <div>Loading...</div>}
{isSuccess && data?.pages && (
<div>
<ListHeader isFetching={isFetchingNextPage} data={data}/>
<InfiniteScroll
dataLength={data.pages.reduce((acc, page) => acc + page.results.length, 0)}
next={fetchNextPage}
hasMore={hasNextPage}
loader={<div>Loading...</div>}
endMessage={<p>- You have seen it all -</p>}
>
{data.pages.map((page, i)=> (
<Fragment key={i+''}>
{page.results.map((item, j)=> (
<ListItem key={i+'.'+j} path={path} item={item}/>
))}
</Fragment>
))}
</InfiniteScroll>
</div>
)}
</div>
Where ListHeader and ListItem are components of your own.

ReactJS conditional display of elements with map function is returning that my object or key is 'undefined'

Here I have passed my state from one component to another the same way I've done before. And when I console.log the passed state (before I add the map statement in JSX) I get the object im looking for. But when I go inside the return statement I get "cannot map 'undefined'". And at that point the console.log starts returning as undefined. But before I tried to map over the passed prop the console.log was returning the data.Can anyone help me see what im doing wrong?
function Items ({orgList}) {
console.log(orgList.items)
return (
<>
{orgList ?
<div>
{orgList.items.map((item, i) => (
<Card key={i} className= "w-100 m-2">
<CardHeader className="d-flex">
<p>Hello</p>
</CardHeader>
<CardBody>
<p>The Body</p>
<Row>
<Col md="5">
<dl>
<dt>GroupID</dt>
<dd>{item.GroupID}</dd>
<dt>Name</dt>
<dd>{item.name}</dd>
</dl>
</Col>
</Row>
</CardBody>
</Card>
))}
</div>
:
<div>
<PageLoadSpinner inProgress={inProgress} />
</div>
}
</>
)
}
export default Items;**strong text**
Here is the parent Component
function Orgs () {
const [orgList, setOrgList] = useState([]);
useEffect(() => {
loadOrgs();
}, []);
const loadOrgs = () => {
api.MonitoringOrg.list()
.then(response => {
console.log(response)
setOrgList(...orgList, response)
})
.catch(err => console.log(err))
}
return(
<>
<Items orgList={orgList}/>
</>
)
}
export default Orgs;
Errors that include undefined like the one you are observing likely mean one of two things:
(1) The variable is not defined or accessible to the function. This is not the case in your situation as you have shown that the variable is accessible outside of the scope of your conditional. Or,
(2) The render function has not yet evaluated your variable by the time it gets to rendering. This is likely the case in your situation.
I think the second situation is due to a race condition, and I leave the details of that to someone with better knowledge of the React source code to explain.
An easy way to avoid this situation is to update your conditional with the specific keys you will need. This will help prevent the render function from short circuiting your component. This way, your data will not be called upon if a render proceeds without it being there.
Given in your situation that you are drawing on orgList.items in your map function, let's update the code as per the following:
function Items ({orgList}) {
console.log(orgList.items)
return (
<>
{orgList?.items? {/* <------ update this line as seen here */}
<div>
{orgList.items.map((item, i) => (
<Card key={i} className= "w-100 m-2">
<CardHeader className="d-flex">
<p>Hello</p>
</CardHeader>
<CardBody>
<p>The Body</p>
<Row>
<Col md="5">
<dl>
<dt>GroupID</dt>
<dd>{item.GroupID}</dd>
<dt>Name</dt>
<dd>{item.name}</dd>
</dl>
</Col>
</Row>
</CardBody>
</Card>
))}
</div>
:
<div>
<PageLoadSpinner inProgress={inProgress} />
</div>
}
</>
)
}
export default Items;

How to cleanup "react-beautiful-dnd" Draggable after removal?

I'm using this awesome react-beautiful-dnd library to let users reorder a list of items by "drag and drop". It works just fine. The only problem I've encountered is about when a user removes one of the items in the list. It seems that the "Draggable" component doesn't cleanup itself after getting unmounted:
const [items, setItems] = useState(initialItems);
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
{items.map((item, index) => {
return (
<Draggable
key={item.id}
draggableId={item.id.toString()}
index={index}
>
{(provided) => (
<div
{...provided.dragHandleProps}
{...provided.draggableProps}
ref={provided.innerRef}
>
<DraggableItem
item={item}
setItems={setItems}
/>
</div>
)}
</Draggable>
);
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
As you can see, "setItems" is passed to "DraggableItem" component so that it can update state after removing an item.
It correctly updates state and everything is just fine but I get this "Warning" on my browsers console:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
How am I supposed to cleanup after updating items by calling "setItems"?

Using conditions in AntD List Actions

I am trying to use conditions for the actions array in the AntD List Component. The following action should only be added, if the user is an admin and the count is >0. But it seems, that I cannot provide conditions like this inside the actions.
<List dataSource={items}
itemLayout="horizontal"
renderItem={item => (
<List.Item
actions={[
{isAdmin && item.count > 0 && <Button onClick={this.moveItemUp(item.key)}>up</Button>,}
<a key="list-loadmore-more">more</a>]}
>
...
</List.Item>
)}
Any ideas how to solve?
React automatically ignores any null or false values in render, so you are safe to leave the values as it exists:
<List dataSource={items}
itemLayout="horizontal"
renderItem={item => (
<List.Item
actions={[
isAdmin && item.count > 0 && <Button onClick={this.moveItemUp(item.key)}>up</Button>,
<a key="list-loadmore-more">more</a>]}
>
...
</List.Item>
)}
></List>
Stackblitz example
EDIT: If you want to avoid passing the item altogether, you can use the spread operator to do so:
actions={[
<a key="list-loadmore-edit">edit</a>,
...(condition ? [<a>Another</a>] : []),
]}
You can move the isAdmin and item.count > 0 condition outside of return and pass dynamically created array to like shown below
import React from 'react';
export default class App extends React.Component {
render() {
let actionList = [<a key="list-loadmore-more">more</a>];
if(isAdmin && items.length > 0) actionList.push(<Button onClick={this.moveItemUp(item.key)}>up</Button>)
return (
# your code
<List dataSource={items}
itemLayout="horizontal"
renderItem={item => (
<List.Item
actions={actionList}
>
...
</List.Item>
)}
# your code
)
}
}

React Reveal not working for array of data

Can't use React Reveal on array of data with .map() to produce effect from documentation.
https://www.react-reveal.com/examples/common/
Their documentation gives a nice example
<Fade left cascade>
<div>
<h2>React Reveal</h2>
<h2>React Reveal</h2>
<h2>React Reveal</h2>
</div>
</Fade>
I want to produce the same CASCADE effect with my data
<React.Fragment>
{projects.filter(project => project.category === category)
.map((project, index) => {
return (
<ProjectThumb key={index} project={project}
showDetails={showDetails}/>
)
})}
</React.Fragment>
The effect I'm getting is that the entire ProjectThumb component list fades in in one group, I need them to fade in individually and as i scroll. Thanks in advance.
Pass react-reveal props to your React component. It will work.
<Fade left cascade>
<div>
{
projects
.filter(project => project.category === category)
.map((project, index) => (
<ProjectThumb key={index} project={project} showDetails={showDetails} />
))
}
</div>
</Fade>
In your ProjectThumb.js
const ProjectThumb = props => {
return <Whatever {...props}>{...}</Whatever>
}

Resources