dynamically add item VS scroll behavior - reactjs

As shown in the minimal example (gif), the scroll-behavior when adding items depends on the current scroll position.
Here is the code for that example:
const [items, setItems] = React.useState([]);
const addItem = i => {
setItems(s => [...s, i]);
};
return (
<div className="App">
<header style={boxStyle}>HEADER</header>
{items.map(item => (
<Item text={item} key={item} />
))}
<button onClick={() => addItem(`item${items.length}`)}>Add</button>
<footer style={boxStyle}>FOOTER</footer>
</div>
);
https://codesandbox.io/s/ancient-feather-lttw5?file=/src/App.js
For the first 2 items the add button moves down. But then the button feels like staying at the same position when new items are added before (effectively the scroll position changes what makes it feel like the item is added "before" rather than "in-place").
In our real world app that UX feels super stange, because each item is a big item-from with 10+ fields and the user would see only the bottom of that form. Instead, we would like the user to see the beginning of the form after clicking the add button (like in the example when adding item0 and item1 where the beginning of the item is shown where the mouse is).
Desired scroll UX
I found out that hiding the add button for a render after adding the item fixes the issue:
const [items, setItems] = React.useState([]);
const addItem = i => {
setHide(true);
setItems(s => [...s, i]);
setTimeout(() => setHide(false), 1);
};
const [hide, setHide] = React.useState(false);
return (
<div className="App">
<header style={boxStyle}>HEADER</header>
{items.map(item => (
<Item text={item} key={item} />
))}
{!hide && (
<button onClick={() => addItem(`item${items.length}`)}>Add</button>
)}
<footer style={boxStyle}>FOOTER</footer>
</div>
);
https://codesandbox.io/s/stupefied-kirch-6s5fs?file=/src/App.js
Question
Is there a (cross-browser (and ie11+)) way to achieve the desired behavior in a more elegant way? Ideally without manipulating the scroll position manually.

I think that your best candidate to do so is the method scrollIntoView(). Maybe when you click your button you can identify your item in the dom and scroll to it whenever the document is scrollable and the first item is out of view. Like:
const handleClick = (item) => {
const firstItem = undefined; //you should find the way to get the first item of your list in the dom
firstItem.scrollIntoView({ block: "start" }); // you can add some options for smoothess and position
addItem(item);
}
UPDATE:
Now that I understand the expected result clear, I still think that scrollIntoView is the solution, but in this case you will need to do it after item adding, you may need the hook useEffect for this, like:
useEffect(()=> {
const myAddButton = document.getElementById('myAddButton'); //get the button element
myButton.scrollIntoView({
block: "end"
});
}, [items])
/* Handle click is not needed anymore
const handleClick = (item) => {
addItem(item);
}*/
You can still tweak this a little bit so you can have a gap between the bottom of the page, but this will make a good start.

Related

Hide Item onScroll React

I'm attempting to hide an item when a user scrolls within a div. The Card component shown is a scrollable component. When the user scrolls within it I want to hide an item. What is the best way to go about this? I'm getting the error: Too many re-renders. Note: Partial code shown
const [scrolling, setScrolling] = useState(false);
const handleScroll = (e) => {
setScrolling(true);
};
return (
<Card onScroll={handleScroll}>
{scrolling ? null : <p> hide me on scroll </p>}
</Card>
);
something you could do is move the scrolling ? null : ... outside of the return and just keep it a variable.
Like this:
const [scrolling, setScrolling] = useState(false);
const handleScroll = (e) => {
setScrolling(true);
};
const item = scrolling ? null : <p> hide me on scroll </p>
return (
<Card onScroll={handleScroll}>
{item}
</Card>
);

scroll to div using useRef in map method

i am using useRef to scroll to specific div but it is not working in map method's case (probably because of id) , so can anyone tell me how to provide id. it is taking last element in map method right now.
this is element to which i want to scroll to.
{allMessages?.map((message) => (
<div
key={message.data.id}
ref={filterRef}>
<div>
<p>{message.data.text}</p>
</div>
</div>
))}
this is filtered data in which i am getting filtered messages and clicks on specific div.
{filteredMsg?.map((item) => (
<li
onClick={() => goToFilterData(item.data.id)}
key={item.data.id}
>
{item.data.text}
</li>
))}
this is what i have done with useRef yet -
const scrollToRef = (ref) => window.scrollTo(0, ref.current.offsetTop);
const goToFilterData = (id) => {
scrollToRef(filterRef);
};
Just pass an id to every element that maybe should scroll into the view.
{allMessages?.map((message) => (
// the element you want to scroll to
<div id={`message_${message.someUniqueIdentifier}`} />
))}
Pass the identifier to the scroll function.
{filteredMsg?.map((item) => (
<li onClick={() => goToFilterData(item.data.someUniqueIdentifier)}>
{item.data.text}
</li>
))}
Query the element and make it scroll into the view.
const goToFilterData = (uniqueIdentifier) => {
const element = document.getElementById('message_' + uniqueIdentifier);
element.scrollIntoView()
};
Note: ofc you could handle this with a lot of single refs and pass them, but this should just work fine.

Browser API, 'draggable' is not selecting proper target in Next.js

First, please check my code.
const [dragItem, setDragItem] = useState(undefined);
const [dragFile, setDragFile] = useState(undefined);
const [targetFolder, setTargetFolder] = useState(undefined);
const handleDragStart = (index) => {
setDragItem(index);
setTargetFolder(undefined);
};
const handleFileDragStart = (index) => {
setDragFile(index);
setTargetFolder(undefined);
}
const handleDrop = (index) => {
setTargetFolder(index);
setIsReady(true);
};
///////////////////////
<div>
{rootFolder?.map((root) => (
<div className={classes.root} key={root.FOLDER_PK}>
<ListItem
button
dense
divider
selected
draggable
onDragStart={() => handleDragStart(root.FOLDER_PK)}
onDrop={() => handleDrop(root.FOLDER_PK)}
onDragOver={(e) => e.preventDefault()}
>
<ListItemIcon>
<Folder />
</ListItemIcon>
<ListItemText/>
<ThreeDotsMenu/>
</ListItem>
</div>
))}
</div>
This code is being rendered like this.
However, when I quickly click and drag the item, there's no problem. However, when I click the item for about 1~2 seconds and drag, It is being dragged like this.
This is a moment when I click 'folder 4' for about 1 second and drag it.
With this comparison, you can see how It's different when I'm click and drag quickly or not.
I'm new to this Browser API, so I need some help. If you want some more information, please add a comment. I'll add my code right away.
Your help will be much appreciated. Thank you!

Is there any pitfall of using ref as conditional statement?

Context: I am trying to scroll view to props.toBeExpandItem item which keeps changing depending on click event in parent component. Every time a user clicks on some button in parent component I want to show them this list and scroll in the clicked item to view port. Also I am trying to avoid adding ref to all the list items.
I am using react ref and want to add it conditionally only once in my component. My code goes as below. In all cases the option.id === props.toBeExpandItem would be truthy only once in loop at any given point of time. I want to understand will it add any overhead if I am adding ref=null for rest of the loop elements?
export const MyComponent = (
props,
) => {
const rootRef = useRef(null);
useEffect(() => {
if (props.toBeExpandItem && rootRef.current) {
setTimeout(() => {
rootRef.current?.scrollIntoView({ behavior: 'smooth' });
});
}
}, [props.toBeExpandItem]);
return (
<>
{props.values.map((option) => (
<div
key={option.id}
ref={option.id === props.toBeExpandItem ? rootRef : null}
>
{option.id}
</div>
))}
</>
);
};
Depending upon your recent comment, you can get the target from your click handler event. Will this work according to your ui?
const handleClick = (e) => {
e.target.scrollIntoView()
}
return (
<ul>
<li onClick={handleClick}>Milk</li>
<li onclick={handleClick}>Cheese </li>
</ul>
)

Rendering large amount of items prevents other functions to work before render completes

I am trying to find out how to prevent rendering a large amount of data freezing other functionalities in a React component.
In the code, I have a button called Render, which renders 30000 items and another button, Reset, that removes them.
Once I click Render, I am unable to click the other button, Reset because the component is busy rendering 30000 items for the time being.
I want to be able to click Reset button while the component is trying to render the items. Please help me with ways to resolve this issue.
const ItemsComponent = () => {
const [displayItems, setDisplayItems] = useState(false);
const items = Array.from(Array(30000)).map((item, index) => {
return (
<li key={index}>
Item {index + 1}
</li>
)
});
const renderItems = () => { setDisplayItems(true) }
const resetItems = () => { setDisplayItems(false) }
return (
<div>
<button onClick={renderItems}>Render</button>
<button onClick={resetItems}>Reset</button>
<ul>
{ displayItems ? items : null }
</ul>
</div>
);
}

Resources