I have an Infinite Scroll with React.js
It is literally Infinite, so I can't measure How many is it.
<div className="InfiniteScroll">
<div ref={observer} className="item">item</div>
<div ref={observer2} className="item">item</div>
{/* Actually I am using map() */}
{...}
</div>
I can add IntersectionObserver on any div.
const observer = new IntersectionObserver(() => {
console.log('Found it');
},{
root: divRef.current,
rootMargin: ...,
});
When I want to know the currently visible divs, How can I know that with this?
Add IntersectionObserver on each div looks not reasonable.
If I want to invoke some function with 1,000th div or random div, how can I achieve it?
you can set any div with code
new IntersectionObserver(function(div) {
div.isIntersecting && do_something();
}).observe(div);
I would suggest you attach a ref to the very last div. So when that last div is intersecting, then you can invoke your function there.
Attaching ref to the last div
{list.map((item, index) => {
if (item.length === index + 1) {
return <div ref={lastItemElementRef} key={item}>{item}</div>
} else {
return <div key={item}>{item}</div>
}
})}
Now when you reach this last div you can have an external API call for infinite scroll and invoke some function
Intersecting logic
const observer = useRef()
const lastItemElementRef = useCallback(node => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
// logic for triggering set state and API call - Infinite scroll
// some other function invokation
}
})
if (node) observer.current.observe(node)
}, [loading, otherdependencies])
Related
I am using ReactJS on an App and currently need to be able to print some elements from the page on user's request (click on a button).
I chose to use the CSS media-query type print (#media print) to be able to check if an element should be printed, based on a selector that could be from a class or attribute on an Element. The strategy would be to hide everything but those "printable" elements with a stylesheet looking like:
#media print {
*:not([data-print]) {
display: none;
}
}
However, for this to work I need to also add the chosen print selector (here the attribute data-print) on every parent element each printable element has.
To do that here's what I've tried so far:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.setAttribute("data-print", "");
element = element.parentElement;
}
element.setAttribute("data-print", "");
}
});
};
const handleAfterPrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.removeAttribute("data-print");
element = element.parentElement;
}
element.removeAttribute("data-print");
}
});
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
With printNodeSelectors being a const Array of string selectors.
Unfortunately it seems that React ditch out all my dirty DOM modification right after I do them 😭
I'd like to find a way to achieve this without having to manually put everywhere in the app who should be printable, while working on a React App, would someone knows how to do that? 🙏🏼
Just CSS should be enough to hide all Elements which do not have the data-print attribute AND which do not have such Element in their descendants.
Use the :has CSS pseudo-class (in combination with :not one) to express that 2nd condition (selector on descendants):
#media print {
*:not([data-print]):not(:has([data-print])) {
display: none;
}
}
Caution: ancestors of Elements with data-print attribute would not match, hence their text nodes (not wrapped by a tag) would not be hidden when printing:
<div>
<span>should not print</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
Demo: https://jsfiddle.net/6x34ad50/1/ (you can launch the print preview browser feature to see the effect, or rely on the coloring)
Similar but just coloring to directly see the effect:
*:not([data-print]):not(:has([data-print])) {
color: red;
}
<div>
<span>should not print (be colored in red)</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
After some thoughts, tries and errors it appears that even though I managed to put the attribute selector on the parents I completely missed the children of the elements I wanted to print! (React wasn't at all removing the attributes from a mysterious render cycle in the end)
Here's a now functioning Component:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
const elements: Element[] = [];
// we need to give all parents and children a data-print attribute for them to be displayed on print
const addParents = (element: Element) => {
if (element.parentElement) {
elements.push(element.parentElement);
addParents(element.parentElement);
}
};
addParents(printableElement);
const addChildrens = (element: Element) => {
elements.push(element);
Array.from(element.children).forEach(addChildrens);
};
addChildrens(printableElement);
elements.forEach((element) => element.setAttribute("data-print", ""));
}
});
};
const handleAfterPrint = () => {
document.querySelectorAll("[data-print]").forEach((element) => element.removeAttribute("data-print"));
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
I usually don't like messing with the DOM while using React but here it allows me to keep everything in the component without having to modify anything else around (though I'd agree that those printNodeSelectors need to be chosen from outside and aren't dynamic at the moment)
I created a filter gallery. I want to animate the filter items every time I click to a buttons. But my codes are not doing it properly. It animates filter items like toggle. If I click on a button first time it animates items, then If I click on another button it shows nothing. After that If I click on another button it animates again. What's wrong with my code? Experts please help me to find out the proper solution. Thanks in advance.
Here is my code:
import React, { useState } from 'react';
import suggestData from '../data/suggest-data.json';
const allCategories = ['All', ...new Set(suggestData.map(item => item.area))];
const Suggest = () => {
const [suggestItem, setSuggestItem] = useState(suggestData);
const [butto, setButto] = useState(allCategories);
const [selectedIndex, setSelectedIndex] = useState(0);
const [anim, setAnim] = useState(false);
const filter = (button) => {
if (button === 'All') {
setSuggestItem(suggestData);
return;
}
const filteredData = suggestData.filter(item => item.area === button);
setSuggestItem(filteredData);
}
const handleAnim = () => {
setAnim(anim => !anim);
}
return (
<div>
<h1>Suggest</h1>
<div className="fil">
<div className="fil-btns">
<div className="fil-btn">
<button className='btn'>Hello</button>
{
butto.map((cat, index) => {
return <button type="button" key={index} onClick={() => { filter(cat); setSelectedIndex(index); handleAnim(); }} className={"btn" + (selectedIndex === index ? " btn-active" : "")}>{cat}</button>
})
}
</div>
</div>
<div className="fil-items">
{
suggestItem.map((item, index) => {
return (
<div className={"fil-item" + (anim ? " fil-item-active" : "")} key={index}>
<h1>{item.name}</h1>
<h2>{item.category}</h2>
<h3>{item.location}</h3>
<h4>{item.type}</h4>
<h5>{item.area}</h5>
</div>
);
})
}
</div>
</div>
</div>
);
}
export default Suggest;
In your handleAnim() function, you are simply toggling the value of anim state. So initially, its value is false and when you click the button for the first time, it is set to true. On clicking the next button, the anim state becomes false because the value of !true is false and hence your animation doesn't work. On next click, becomes true again since !false is true and the toggle continues again and again.
If you want to make your animations work on every click you will need to set the anim state value to true on every button click as below since you seem to depend on the value to set animations. As an alternative, I think it will do just fine if you simply add the animation directly to the enclosing div with class .filter-item instead of relying on states to trigger the animation since after every filter you apply, the elements will be re-rendered and the animation will happen after every re-render.
const handleAnim = () => {
setAnim(true);
}
I am trying to have the animation work everytime i click on the buttons in the below project
Code - https://codesandbox.io/s/8fjs8i
Description
There are 3 buttons named first, second and third. when i click on first, the purple box below shows first, when i click on second it shows second and so on, basically the div is updating everytime based on button click.
Problem : but the animation fadIn that i have given to the div works only on application load. how can i make it work everytime i click the button so the the box fadesIN with animation for every click.
const Renders = ({ arr }) => {
const [load, setLoad] = useState(false);
useEffect(() => {
setLoad(true);
setTimeout(() => {
setLoad(false);
}, 1);
}, [arr]);
if (load) return <></>;
return (
<div className="renders">
<div className="zoomers">{arr}</div>
</div>
);
};
This another way you can do it:
const Renders = ({ arr }) => {
const [load, setLoad] = useState(false);
useEffect(() => {
setLoad(true);
setTimeout(() => {
setLoad(false);
}, 1);
}, [arr]);
return (
<div className="renders">
<div className={load ? '' : "zoomers"}>{arr}</div>
</div>
);
};
Now look the different, before you load and unload, now its just a play with the class name, the idea that every time you load element with the animation class so its start to work
I have a react app in which I'm trying to make each table row fade out whenever a corresponding button to that table row is clicked. The idea is that when a delete button on that row is clicked the whole row will fade out and the rest of the rows below it will move up (without creating any empty space) and if it is the last row that is being removed it will just fade out and table will become smaller.
Currently I'm trying to use refs and CSS to implement this feature, however right now if, for example, I click on "Delete" button in the second row - this row will be removed immediately without any animation, but at the same time the very last row's content will fade out according to the CSS styling that I set up and there will be an empty table row left there. Following that, any other element will be removed without any fade out animation.
So far I have done the following:
1) A component that create dynamic table rows (any necessary data, including the ref, are passed through useContext() from the component with the business logic):
function TableData() {
const { dataset, handleDeletion, wrapperRef } = React.useContext(Context)
return dataset.map((item, index) => {
const { id, name, year, color, pantone_value } = item
return (
<tr key={id} ref={wrapperRef}>
<td>{year}</td>
<td>{name}</td>
<td style={{ backgroundColor: color, width: "100px" }} />
<td>
<button
value={id}
type="button"
class="btn btn-outline-danger"
onClick={handleDeletion}>
<i class="far fa-trash-alt"></i>
</button>
</td>
</tr>
)
})
}
2) In the main component with business logic I set up the ref:
const wrapperRef = React.createRef()
3) And this is a function that handle a click of a delete button:
function handleDeletion(event) {
const id = event.currentTarget.value
const wrapper = wrapperRef.current
wrapper.classList.add('fade')
setDataset(prevDataset => prevDataset.filter(item => item.id !== parseInt(id)))
}
4) Simple CSS animation:
.fade {
opacity: 0;
transition: opacity 5000ms;
}
Could you please let me know what am I doing wrong here and what would need to be fix in order to make it work as planned?
Best regards,
Konstantin
P.S. I'm attaching the full code to this post as well (just in case) and here is the link to the pen with this project.
UPDATE:
1) I added the following className into my 'tr' component:
<tr key={id} className={toDelete && itemToDelete === id ? 'fade' : ''}>
2) In the main component I added the following state hooks:
const [ toDelete, setToDelete ] = React.useState(false)
const [ itemToDelete, setItemToDelete ] = React.useState('')
3) And changed the delete function to the following (with adding an extra useEffect that is applied with those hooks being changed):
function handleDeletion(event) {
const id = event.currentTarget.value
setToDelete(true)
setItemToDelete(id)
//setDataset(prevDataset => prevDataset.filter(item => item.id !== parseInt(id)))
}
React.useEffect(() => {
if (toDelete !== false && itemToDelete !== '') {
setTimeout(() => {
setDataset(prevDataset => prevDataset.filter(item => item.id !== parseInt(itemToDelete)))
}, 500)
}
}, [toDelete, itemToDelete])
I tried to do it different ways, including using setTimeout/setInterval, doing it without a separate useEffect hook in the handleDeletion function, etc. But still can't get the desired result, it is removing it without applying fading animation.
You create wrapperRef and you overwrite it with every map iteration so that it holds the reference on the last table row. That is why you see the last row fade out.
You would probably need an array of refs so that you have the ref for every row. Depending on how big the table is maybe you should look for other solutions because it not recommended to overuse refs
This approach is probably the best.
In addition to Darko's answer, you can also delete the row from the DOM after fading out.
setTimeout(function() { that.setState({ isDeleted: true })}, 500);
https://codepen.io/primaryobjects/pen/qBaMvrq
I'm working on a chat app and am using the scroller from bottom to top to load older messages.
When a new message arrives I want to check first if the user is at the bottom of the div, and only then use a scrollToBottom function.
How can I get the current height/position of the user?
https://www.npmjs.com/package/react-infinite-scroller
Thank you,
Omri
Unfortunately it's been a few days without reply. This is my workaround:
I created a boolean called isBottom, and attached onScroll={handleScroll} function to my messages div.
const [isBottom, setIsBottom] = useState(true);
const scrollToBottom = (behavior) => {
messagesEndRef.current.scrollIntoView();
};
const handleScroll = (e) => {
const bottom =
e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
if (bottom) {
setIsBottom(true);
} else {
setIsBottom(false);
}
};
The messages div:
<div className="msg_list" onScroll={handleScroll}>
<InfiniteScroll
loadMore={loadMore}
initialLoad={true}
hasMore={hasMoreItems}
loader={<LoadingAnimation key={0} />}
useWindow={false}
isReverse={true}
>
{messages}
<div ref={messagesEndRef} />
</InfiniteScroll>
</div>
And then I added a useEffect to handle changes from my messages array (arriving from props)
useEffect(() => {
if (isBottom) {
scrollToBottom();
} else {
setUnreadMessages((unreadMessages) => unreadMessages + 1);
}
}, [messageList]);
* BTW you also need to wire the scrollTobottom function to your send message box, since if you are the one who sent the message it should scrollToBottom anyway