scroll to div using useRef in map method - reactjs

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.

Related

Why is useState and DOM not recreating/identifying/re-rendering the new state even with useState([...Array])?

I am trying to render and then re-render using array.map() an array of objects. The useEffect is called whenever the state(selected) changes for the options ('All','BACKEND','FRONTEND','ML').
The
All Selected rendering all data
Backend Selected showing "backendData"
Frontend Selected etc etc
MY ISSUE:
The rendering of the data is fine but I have added a basic animation which fades the elements in one by one using "react-awesome-reveal" library. I did the same with plain CSS so I don't think my error lies here.
The animation works if you refresh or direct towards the page or THE NUMBER OF ELEMENTS TO BE RE-RENDERED IS MORE THAN THE ELELEMNTS ON SCREEN THE LATER ELEMENTS WILL HAVE THE ANIMATION.
EG: If I go from "Frontend" to "Backend" then there are currently 2 elements in the DOM but now 3 need to be re-rendered so the first 2 elements do not carry the animation but the third does. It also works in the reverse where if I go from "All" 9 elements to "ML" 2 elements the elements are automatically rendered without the animation.
It seems like the DOM is looking at what is already rendered and deciding to not re-render the elements that are already there thus the FADE animation is not working.
Ideally I want the setData(...) to create a new state so that the array is mapped as if it the page was refreshed.
Here is the code snippet:
import {
backendCourses,
frontendCourses,
mlCourses,
allCourses,
} from "../../data";
export default function Courses() {
const [selected, setSelected] = useState();
const [data, setData] = useState(allCourses);
const list = [
{ id: "all", title: "All" },
{ ....},
...
];
useEffect(() => {
switch (selected) {
case "all":
setData(Array.from(allCourses));
break;
case "backend":
setData([...backendCourses]);
case ....
}, [selected]);
return (
<div className="courses" id="courses">
<h1>Courses</h1>
<ul>
{list.map((item, index) => (
<CoursesList
...
active={selected === item.id}
setSelected={setSelected}
/>
))}
</ul>
<div className="container">
{data.map((item, index) => (
<Fade ...>
<div ...>
<img ...>
</div>
</Fade>
))}
</div>
</div>
);
}
//CoursesList Component
export default function CoursesList({ id, title, active, setSelected }) {
return (
<li
className={active ? "coursesList active" : "coursesList"}
onClick={() => setSelected(id)}
>
{title}
</li>
);
}
The mapping of the elements using Fade from "react-awesome-reveal" FULL-CODE.
<div className="container">
{data.map((item, index) => (
<Fade delay={index * 500}>
<div className="item">
<img src={item.img} alt={item.title} id={index}></img>
<a href={item.url} target="_blank">
<h3>{item.title}</h3>
</a>
</div>
</Fade>
))}
Here is the full useEffect hook code. I initially thought I was not creating a new array only referencing it but have tried multiple ways to clone the array without success. The courses data for now is hard-coded and just read from a file.
useEffect(() => {
switch (selected) {
case "all":
setData(Array.from(allCourses));
break;
case "backend":
setData([...backendCourses]);
break;
case "frontend":
setData([...frontendCourses]);
break;
case "ml":
setData([...mlCourses]);
break;
default:
setData([...allCourses]);
}
}, [selected]);
Sorry for the long post but trying to be as detailed as I can.
It seems like the DOM is looking at what is already rendered and deciding to not re-render the elements that are already there thus the FADE animation is not working.
It's React that does that, not the DOM. That's part of its job: To avoid recreating DOM elements that already have the content you're trying to show.
If you want React to think you're providing new elements every time, even when in fact they're the same, give the elements a new key. At the moment, your map call isn't giving them a key at all (which it should be), so React defaults to using the element's position in the list as a key.
Instead, provide a key on the Fade component and make sure it changes when selected changes, probably by using selected itself in the key:
return (
<div className="container">
{data.map((item, index) => (
// I'm assuming `item.url` is unique within the array, adjust this
// if not. (*Don't* use `index`, indexes make poor keys, see the
// link above.
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
<Fade key={selected + "::" + item.url} delay={index * 500}>
<div className="item">
<img src={item.img} alt={item.title} id={index}></img>
<a href={item.url} target="_blank">
<h3>{item.title}</h3>
</a>
</div>
</Fade>
))}
</div>
);
That way, the elements get reused correctly when the component is rendered for other reasons (because each of them has a stable key), but when you change selected, they all get replaced with new elements because the key changes.

Removing a specific element in an array with React

I have this code
I have some sections and inside it I have an item, number and button. How can I remove some specific item?
I'm rendering the sections as:
{sections.map((section, index) => (
<Section
section={section}
key={section.id}
addItem={(item) => addItem(index, item)}
removeItem={(i) => removeItem(index, i)}
/>
))}
And in my Section component, I'm rendering it as:
{section.items.map((item, i) => (
<>
<h2>{item}</h2>
<h3>{section.number[i]}</h3>
<button onClick={() => removeItem(i)}>Remove</button>
</>
))}
The remove function is in the Section's parent component and is:
const removeItem = (index, i) => {
let filteredItem = sections[index].items.splice(i);
let filteredNumber = sections[index].number.splice(i);
setSections((prev) => ({ ...prev, filteredItem, filteredNumber }));
};
When I click the remove button it says that sections.map is not a function. What am I doing wrong?
At glance looks like you have an error in your Section component. The code:
{section.items.map((item, i) => (
<>
<h2>{item}</h2>
<h3>{section.number[i]}</h3>
<button onClick={() => removeItem(i)}>Remove</button>
</>
))}
should be:
{section.items.map((item, i) => (
<>
<h2>{item.title}</h2>
<h3>{item.number[i]}</h3>
<button onClick={() => removeItem(i)}>Remove</button>
</>
))}
Because you're passing down item and you're trying to render everything in the item object and section.number should be item.number.
I see your problem.
In the code below when removing an element you are setting the sections to a single object not an array anymore. so map() doesn't exist on an Object. You have to convert it back into an Array.
const removeItem = (index, i) => {
let filteredItem = sections[index].items.splice(i);
let filteredNumber = sections[index].number.splice(i);
setSections((prev) => ({ ...prev, filteredItem, filteredNumber }));
};
Edit:
Upon further inspection of your code I see more errors.
In the remove item section in Section.js i see your trying to
const itemTarget = section.items[i];
That seems to be cause you are acting as is section is one object. but its an array that already has one section so you have to call it as follows for it to grab the items from the first (default) section.
const itemTarget = section[0].items[i];
This is the same with the filtered variable you will have to make sure when removing the item you are removing it from the correct section aswell.

Possible to render components in reverse manner in JSX?

In my Nextjs/React.js component I am rendering list of cards like this :
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
<div className="lg:col-span-8 col-span-1">
{posts.map((post, index) => (
<PostCard post={post.node} key={post.title} />
))}
</div>
I was wondering if it was possible to render these PostCards in a reverse manner; starting from the last index rather than the initial index? This is for my blog application and whenever I post a new PostCard I want the latest Post to be rendered on Top of the list instead of the bottom of the list.
Just reverse the array first:
{posts.slice(0).reverse().map((post, index) => (
<PostCard
post={ post.node }
key={ post.title }
/>
))}
whenever I post a new PostCard I want the latest Post to be rendered on Top of the list instead of the bottom of the list.
If you're currently doing this by adding to the end of an array, you can add to the start of it instead. For example, if you're doing this:
setPosts(prev => [...prev, { title: 'My New Post' }]);
Do this instead:
setPosts(prev => [{ title : 'My New Post' }, ...prev]);
If you can't change the way the array gets created (say, because some components want it in one order, and some in another), then you can create a new array in the right order, and then map over that. You may want to memoize this if the array isn't going to change often:
const reorderedPosts = useMemo(() => {
return [...posts].reverse();
}, [posts]);
// ...
{reorderedPosts.map((post, index) => (
<PostCard post={post.node} key={post.title} />
))}
This can also easily be enhanced to let you change the order via a state, if you need to:
const [shouldReverse, setShouldReverse] = useState(false);
const reorderedPosts = useMemo(() => {
if (shouldReverse) {
return [...posts].reverse();
} else {
return posts;
}
}, [posts, shouldReverse])
// ...
{reorderedPosts.map((post, index) => (
<PostCard post={post.node} key={post.title} />
))}

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>
)

Jest: functional component array list not mapping after adding element

I am trying to list person by clicking onClick function to add empty object so that map can loop
over and show div. I see prop onclick function calling works but i map is not looping over it.
a little help would be appreciated.
// functional component
const listing = ({list=[]}) => (
<>
{
<div id='addPerson' onClick={() => list.push({})}>Add person</div>}
{list.map((element, index) => (
<div key={index} className='listItems'>
<p>list name</p>
</div>
))}
</>
);
export default listing;
// test case
it('renders list-items', () => {
const wrapper = shallow(<listing />);
wrapper.find('#addPerson').prop('onClick')();
console.log(wrapper.find('.listItems').length); // showing zero, expected 1
});
Updating List is not going to cause a re-render in your component. I am not sure where List is coming from, but I am assuming it is coming from a local state in the parent, or probably redux store. From inside your component, you will have to call a function in your parent to update the list array there.
Below is a version of your component, assuming the list in your local state. You will have to adjust the code to update the state in the parent or redux store, but the below should be enough to give you an idea of what to do. Also, don't push the element to array, as it mutates the object and will not contact re-render.
const Listing = ({list = [], addToList}) => {
return <>
{
<div id='addPerson' onClick={() => addToList({})}>Add person</div>}
{list.map((element, index) => (
<div key={index} className='listItems'>
<p>list name</p>
</div>
))}
</>
};

Resources