While learning how to use react-beautiful-dnd library, I've found that (when I'm building the library required structure) if I define components outside the main app, it works on React but it does not work on Gatsby (development), it starts to work but then errors appear
But if I move those components inside the main app, then it also works on Gatsby
The example code:
export default () => {
const [elements, setElements] = useState(myElements)
const onDragEnd = result => {
const { destination, source} = result
if (!destination) return
if (destination.droppableId === source.droppableId && destination.index === source.index) return
const new_elements = reorder(elements, source.droppableId, destination.droppableId, source.index, destination.index)
setElements(new_elements)
}
const Columns = ({ arr }) =>
arr.map((col, i) => (
<Droppable droppableId={col.columnId} key={i}>
{(provided, snapshot) => (
<Ul ref={provided.innerRef}>
<Elements arr={col.items} />
{provided.placeholder}
</Ul>
)}
</Droppable>
))
const Elements = ({ arr }) =>
arr.map((el, j) => (
<Draggable draggableId={el.id} index={j} key={j}>
{(provided, snapshot) => (
<Li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
{el.text}
</Li>
)}
</Draggable>
))
return (
<DragDropContext onDragEnd={onDragEnd}>
<Grid>
<Columns arr={myElements} />
</Grid>
</DragDropContext>
)
}
So if Columns and Elements are defined outside, it's not working (but it is with React)
Why is this happening?
(the sandbox: https://codesandbox.io/s/using-react-beautiful-dnd-with-hooks-bfwzl?fontsize=14&hidenavigation=1&theme=dark)
As said in the docs https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/draggable.md#keys-for-a-list-of-draggable-
Your key should not include the index of the item
Once changed this, it works as expected
<Droppable droppableId={col.columnId} key={col.columnId}>
<Draggable draggableId={el.id} index={j} key={el.id}>
Related
I have this javascript draggable tabs component, it is a copy from another question here: mui-tablist with react beautiful dnd not auto scrolling on drag and it has a codesandbox
My aim is to migrate to typescript and eventually create something similar to the following mockup.
Migrating to typescript: I have taken the above question/answer and merged them together as best I can
import React from 'react';
import TabContext from "#mui/lab/TabContext";
import TabPanel from "#mui/lab/TabPanel";
import { DragDropContext, Droppable, DropResult } from "react-beautiful-dnd";
import { Draggable } from "react-beautiful-dnd";
import { Stack, styled, Tab, Tabs } from "#mui/material";
interface DraggableTabProps {
label: string;
value: string;
index: number;
key: number;
child: JSX.Element;
};
const DraggableTab = ( props: DraggableTabProps ) => {
return (
<Draggable
draggableId={`${props.index}`}
index={props.index}
disableInteractiveElementBlocking
>
{(draggableProvided) => (
<div
ref={draggableProvided.innerRef}
{...draggableProvided.draggableProps}
>
{React.cloneElement(props.child, {
...props,
...draggableProvided.dragHandleProps
})}
</div>
)}
</Draggable>
);
}
const StyledTabs = styled(Tabs)();
const StyledTab = styled(Tab)();
export default function DraggableTabComponent() {
const [value, setValue] = React.useState('1');
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
setValue(newValue);
};
const [tabs, setTabs] = React.useState(
[...Array(25)].map((_, index) => ({
id: `tab${index + 1}`,
label: `Tab ${index + 1}`,
value: `${index + 1}`,
content: `Content ${index + 1}`
}))
);
const onDragEnd = (result: DropResult) => {
const newTabs = Array.from(tabs);
const draggedTab = newTabs.splice(result.source.index, 1)[0];
newTabs.splice(result.destination!.index, 0, draggedTab);
setTabs(newTabs);
};
return (
<TabContext value={value}>
<Stack>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="1" direction="horizontal">
{(droppableProvided) => (
<StyledTabs
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
value={value}
onChange={handleChange}
variant="scrollable"
scrollButtons
allowScrollButtonsMobile
>
{tabs.map((tab, index) => {
const child = (
<StyledTab label={tab.label} value={tab.value} key={index} />
);
return (
<DraggableTab
label={tab.label}
value={tab.value}
index={index}
key={index}
child={child}
/>
);
})}
{droppableProvided ? droppableProvided.placeholder : null}
</StyledTabs>
)}
</Droppable>
</DragDropContext>
</Stack>
{tabs.map((tab, index) => (
<TabPanel value={tab.value} key={index}>
{tab.content}
</TabPanel>
))}
</TabContext>
);
}
This displays the tabs and I am able to click each tab and view its content, but I get an error along with each click for the draggable, e.g. on the first tab:
react_devtools_backend.js:4026 react-beautiful-dnd Unable to find draggable with id: 0
Could use some help to fix this up, thanks
It's a known issue with react-beautiful-dnd package, and it occurs only in dev mode and React v18.
Strict mode checks are run in development mode only; they do not
impact the production build.
Apparently, to solve it you have to disable strict mode for the part of your application that uses your Tab component which is not the best solution, or just ignore the issue because it is not present in the production mode.
I hope my approach to this is correct but I am trying to build a component in React where a bunch of posts are rendered in a column view stacked on top of one another, each individual post is set to 100vh.
Psuedo Code
<container ref={postRef}>
<post 1/>
<post 2/>
<post 3/>
</container>
I have a ref on the <container/> and from another component's onClick function I get the current index of the <post/> I clicked on, and then find that element in the dom.
useEffect(() => {
if (postRef.current && indexOfClickedPost) {
const refBounds = postRef.current.children[indexOfClickedPost].getBoundingClientRect()
window.scrollTo({
top: // which unit goes here?
});
}
})
my end goal is to scroll my <container/> component to the very top of the post relative to the one that was just clicked.
Ex. if the user clicks a post with an index of 2, the page will scroll to where the post 2 div begins
I am not sure which unit I am to put in my window.scrollTo to get that effect. Putting refBounds.top does not yield the results I want, it seems to be doing nothing at all from what I can visibly see. Any advice would be great. Thanks!
I propose two different approaches, the first one follows your trial, and it makes use of getBoundingClientRect() to calculate the right distance from top and scrollTo to scroll to that element:
function App() {
const [currPostIdx, setCurrPostIdx] = useState(0);
const containerRef = useRef();
const selectPost = (idx) => {
setCurrPostIdx(idx);
};
useEffect(() => {
const el = containerRef.current.children[currPostIdx];
const top = window.pageYOffset + el.getBoundingClientRect().top;
console.log(currPostIdx, top);
window.scrollTo(0, top);
}, [currPostIdx]);
return (
<>
<ul className="navbar">
{posts.map((p, i) => (
<li key={p.title} onClick={() => selectPost(i)}>
{p.title}
</li>
))}
</ul>
<div ref={containerRef}>
{posts.map((p, idx) => (
<Post key={p.title} post={p} idx={idx} />
))}
</div>
</>
);
}
const Post = ({ idx, post }) => (
<div
id={post.title}
className="post"
style={{ backgroundColor: `#${idx + 5}${idx * 3}${idx * 4}` }}
>
<h4>{post.title}</h4>
</div>
);
Demo HERE
The seond approach makes use of hash navigation and hence it has not to calculate the position of the element manually:
function App() {
return (
<div>
<div className="navbar">
{posts.map((p) => (
<a href={`#${p.title}`}>{p.title}</a>
))}
</div>
{posts.map((p, idx) => (
<Post key={p.title} post={p} idx={idx} />
))}
</div>
);
}
const Post = ({ idx, post }) => (
<div
id={post.title}
className="post"
style={{ backgroundColor: `#${idx + 5}${idx * 3}${idx * 4}` }}
>
<h4>{post.title}</h4>
</div>
);
Demo HERE
I was using React-virtualized table and it has a very useful prop called scrollToIndex that was doing the job.
However, I had to get rid of the library and use my own JSX.
Now I have a big list of projects as follows:-
render() {
return(
<div className="container">
{projects.map(project => (
<ProjectRow
key={project.id}
project={project}
/>
))}
)};
Now let's assume projects array has more than 100 projects and I want to scroll initially to the middle of it, let's say project at index 50.
How to do that?
I tried using useRef() and createRef() on the div itself but without any scucess.
You can use scrollIntoView method to get into the specific position
export default function App() {
const [projects, setProjects] = useState([]);
useEffect(() => {
let p = [];
for (let i = 0; i < 300; i++) {
p.push({
projectId: i,
projectName: `Project ${i}`
});
}
setProjects(p);
}, []);
function scrollTo(position) {
const project = document.getElementById(position);
project.scrollIntoView();
}
return (
<div className="App">
<button onClick={() => scrollTo(50)}>Scroll to 50</button>
<button onClick={() => scrollTo(150)}>Scroll to 150</button>
<button onClick={() => scrollTo(250)}>Scroll to 250</button>
{projects.map((px, index) => (
<Project
key={index}
projectName={px.projectName}
projectId={px.projectId}
/>
))}
</div>
);
}
Please find the complete example in the codesandbox https://codesandbox.io/s/happy-jennings-o7tob
I have 3 components.
In ListCard.js, I map cards array and based on the card the user click on, I call handleChangeCardData to update the modal's text.
My question is: How do I update/change the modal's text when my handleChangeCardData function is inside ListCard.js and my modal is on the same level. (Both are in Board.js)
Board.js
const [cardTitle, setCardTitle] = useState("");
return (
{columns.map((column, index) => (
<div className="column__container" key={index}>
<div className="column__header">
<div className="columnHeader__name">
<p>{column.name ? column.name : "..."}</p>
</div>
<div className="columnHeader__button">
<button
className="btn btn-sm --create-card-btn"
data-bs-toggle="modal"
data-bs-target="#modal-card"
onClick={() => setColumnId(column.id)}
>
New item
</button>
</div>
</div>
<Droppable droppableId={column.id}>
{(provided, snapshot) => (
<div
className="column"
ref={provided.innerRef}
{...provided.droppableProps}
>
<ListCard columnId={column.id} />
{provided.placeholder}
</div>
)}
</Droppable>
</div>
))}
<ViewCardModal cardTitle={cardTitle} />
)
LisCard.js
const handleChangeCardData = (cardTitle) => {
setCardTitle(cardTitle);
}
return (
{cards.map((card, index) => (
<>
<div key={index}>
<Draggable draggableId={card.id} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div
className="card --listcard-card"
onClick={() => handleChangeCardData(card.title)}
data-bs-toggle="modal"
data-bs-target="#modal-card-details"
style={{ border: `2px solid ${card.color}` }}
>
<div className="card-body">
<p>{card.title}</p>
</div>
</div>
</div>
)}
</Draggable>
</div>
</>
))}
)
ViewCardModal.js
function ViewCardModal(props) {
return (
<div>{props.cardTitle}</div>
)
}
In general, lift state up. In this case, it sounds like that means moving the state into Board and then passing that state to whatever child components need it (as a prop), and the state setter to whatever (other) child components need it.
Here's a minimal example of lifting state up. I haven't tried to recreate the full complexity of your example, just to provide an example of Parent having state that ChildA uses and ChildB sets:
const {useState} = React;
const ChildA = React.memo(({counter}) => {
console.log("ChildA rendered");
return <div>Counter = {counter}</div>;
});
const ChildB = React.memo(({setCounter}) => {
console.log("ChildB rendered");
return <input
type="button"
value="Increment Counter"
onClick={() => setCounter(c => c + 1)}
/>;
});
const Parent = () => {
const [counter, setCounter] = useState(0);
return (
<div>
<ChildA counter={counter} />
<ChildB setCounter={setCounter} />
</div>
);
};
ReactDOM.render(<Parent />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.development.js"></script>
If there are several levels of hierarchy between where the state is being held and a descendant component that needs it, you might use context instead of props (although you might also look at component composition instead). See those links for details.
you cant do that directly, but must use props.
in list:
onClick={() => props.onClick(card.title)}
in board:
handleChangeCardData = (cardTitle) => {
setCardTitle(cardTitle);
}
<ListCard columnId={column.id} onClick={(e)=>handleChangeCardData(e)}/>
Inside ListCard:
const ListCard = ({setCardTitle}) => {...}
onClick={() => setCardTitle(card.title)}
In the parent:
<ListCard columnId={column.id} setCardTitle={setCardTitle} />
I am converting class based components to react hooks. I got confused on using the ref parts. Because, the way I am using it complains me that The "innerRef" API has been removed in styled-components v4 in favor of React 16 ref forwarding, use "ref" instead like a typical component..
How do i make it work when using hooks?
const Tabs = ({activeTab, children}) => {
const [tabsElements, setTabsElements] = useState([])
return (
<TabsContext.TabProvider activeTab={activeTab}>
<TabsContext.TabConsumer>
{value => (
<ReactTabs>
<TabsContainer>
<ListTabs>
{value.context.tabs.map(tab => (
<TabTitleItem
key={tab.id}
onClick={value.context.onClick(tab)}
id={tab.id}
innerRef={tabElement => {
if (!tabsElements[tab.id]) {
setTabsElements(tabElements => ({
...tabElements,
[tab.id]: tabElement,
}))
}
}}
isActiveTab={value.context.activeTab.id === tab.id}
>
<TabAnchorItem>{tab.title}</TabAnchorItem>
</TabTitleItem>
))}
</ListTabs>
<ActiveTabBorder
activeTabElement={tabsElements[value.context.activeTab.id]}
/>
</TabsContainer>
{children}
</ReactTabs>
)}
</TabsContext.TabConsumer>
</TabsContext.TabProvider>
)
}
Here is the demo
https://codesandbox.io/s/z3moq8662p
First of all you cannot update state within the ref callback method. Secondly you simply need to pass ref instead of innerRef to the TabTitleItem component since it internally handles ref using forwardRef
const Tabs = ({ activeTab, children }) => {
const [tabsElements, setTabsElements] = useState([]);
const tabElements = useRef({});
return (
<TabsContext.TabProvider activeTab={activeTab}>
<TabsContext.TabConsumer>
{value => (
<ReactTabs>
<TabsContainer>
<ListTabs>
{console.log("value", value.context)}
{value.context.tabs.map(tab => (
<TabTitleItem
key={tab.id}
onClick={value.context.onClick(tab)}
id={tab.id}
ref={tabElement => {
tabElements.current[tab.id] = tabElement;
}}
isActiveTab={value.context.activeTab.id === tab.id}
>
<TabAnchorItem>{tab.title}</TabAnchorItem>
</TabTitleItem>
))}
</ListTabs>
<ActiveTabBorder
activeTabElement={tabsElements[value.context.activeTab.id]}
/>
</TabsContainer>
{children}
</ReactTabs>
)}
</TabsContext.TabConsumer>
</TabsContext.TabProvider>
);
};
Working demo