-
-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Variable number of sections #2
Comments
The hook just needs access to the DOM Element to get the position.. you could use plain JS, like Then we might need to tweak that line, because of |
We solve it like so; const refs = React.useRef<HTMLElement[]>([])
const activeScreen = useScrollSpy({
sectionElementRefs: refs.current,
})
...
{items.map((item, i) =>
<div key={i} ref={ref => !refs.current.includes(ref) && refs.current.push(ref)}>section</div>
)} |
Does that seem stable? I don't see any logic for what to do when a section is removed so surely you'd end up with some dangling references. |
We are fortunate that we don’t have to worry about that in our situation, though it’s a good question. Would have to pass a function down to each mapped component that filters the ref in a useEffect cleanup |
@tremby the scrollspy feature is trivial to build with useIntersection Just watch every element/section, and when it enters viewport, set it's id to callback/context/whatever and toggle the active item in menu linking to that element/section. |
@MiroslavPetrik thanks for the tip, I ended up using this method |
I solved it using
Here's a complete example (with sticky menu, smooth scrolling, and way more) import { Card, CardContent, Container, Grid, MenuItem, MenuList, Paper } from '@mui/material';
import { createRef } from 'react';
import { Title, useTranslate } from 'react-admin';
import ReactMarkdown from 'react-markdown';
import useScrollSpy from 'react-use-scrollspy';
const sections = [
'content.you',
'content.how',
'content.start',
'content.advantages',
'content.feedback',
'content.privacy',
'content.closingMessage',
];
export const Homepage = () => {
const translate = useTranslate();
const sectionRefs = useMemo(() => sections.map(() => createRef<HTMLDivElement>()), []);
const activeSection = useScrollSpy({
sectionElementRefs: sectionRefs,
offsetPx: -80,
});
const handleMenuItemClick = (event: React.MouseEvent<HTMLLIElement, MouseEvent>) => {
const { section } = event.currentTarget.dataset;
const ref = sectionRefs.find((ref) => ref.current?.id === section);
ref?.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
};
return (
<Container maxWidth="lg">
<Grid container spacing={4}>
<Grid item xs={12} sm={8}>
<Title title="Homepage" />
{sections.map((section, index) => {
const content = translate(section).trim().replace(/\t/g, '');
const ref = sectionRefs[index];
return (
<Card key={section} id={section} ref={ref} sx={{ mb: 2 }}>
<CardContent>
<ReactMarkdown>{content}</ReactMarkdown>
</CardContent>
</Card>
);
})}
</Grid>
<Grid item xs={12} sm={4}>
<Paper
sx={{
position: 'sticky',
top: 80,
maxHeight: 'calc(100vh - 8rem)',
overflowY: 'auto',
}}
>
<MenuList>
{sections.map((section, index) => {
return (
<MenuItem
key={section}
selected={index === activeSection}
sx={{ transition: 'background-color 0.5s ease-in-out' }}
onClick={handleMenuItemClick}
data-section={section}
>
{section}
</MenuItem>
);
})}
</MenuList>
</Paper>
</Grid>
</Grid>
</Container>
);
}; |
Mm, no, not really. I've tried exactly that more than once over the years. It's fine if all your sections are very short, but IntersectionObserver is not well suited to elements which can be taller than the viewport, and if you're supporting mobile devices that's very likely to be the case. You might suggest targeting the headings; they're likely to always be shorter than the viewport. But that doesn't help, it's the length of the sections which matters. Imagine for example that section 1 is taller than the viewport -- if we scroll down until we see the section 2 heading, but then start scrolling up again, section 2 is still listed active even though we can't see it, and the section 1 heading might still be pages away. It's not a simple problem to solve. See w3c/IntersectionObserver#124 for a relevant discussion. |
This seems like an approach that ought to work, I think. I hadn't thought of using However, you don't have To fix that I think you should just be able to do const sectionRefs = useMemo(() => sections.map(() => createRef<HTMLDivElement>()), [sections]); Do refs need to be cleaned up? I don't think these will; references will remain in the memo table. Probably not a big deal. |
The sections I used are imported so no need to have those as a dependency. I am not aware of refs needing to be cleaned up. |
Even so, it wouldn't hurt. If not cleaned up I think there will still be references to the old DOM nodes, and they will never be garbage collected until React decides to purge the memo table, which to my understanding currently never happens. But like I said it's probably not a big deal, at least unless the app is changing these sections a lot and is long running. |
Any thoughts on what I can do if I have a variable number of sections to watch?
I can't run useRef in a loop (it's against the "rules of hooks").
The text was updated successfully, but these errors were encountered: