I have a simple app with a Header.tsx component that is supposed to be available to all pages and a Home.tsx in which the most of the content will be.
Home.tsx hosts a intersectionObserver that sends data with useContext hook (called homeLinks) to Header.tsx trough App.tsx, so I can update the navigation with class of the currently intersected section. The problem here: anytime the observer updates the context, the whole DOM re-renders, and since Home will hold the brunt of the application that can't be the optimal solution. Could someone explain to me how to achieve this the correct way?
Could I somehow sends refs from Home to Header and set up the observer there?
CodeSandbox demo is available here
App
import homeLinks from "./contexts/homeLinks";
function App() {
const [homePart, setHomePart] = useState<string>(""); //Home part which is observed, setting function
console.log("App is rendered");
return (
<>
<homeLinks.Provider value={{ homePart, setHomePart }}>
<Header /> {/* Outside router, meant to be shared with other pages */}
<Home /> {/* Will be in react router */}
</homeLinks.Provider>
</>
);
}
export default App;
Home
import homeLinks from "../contexts/homeLinks";
export const Home = () => {
const homeParts = useRef(new Array());
const { setHomePart } = useContext(homeLinks);
useEffect(() => {
const menuObserver = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting)
setHomePart(
entry.target.getAttribute("id")
? entry.target.getAttribute("id")!
: "ntn here",
);
},
{ rootMargin: "-40% 0px -50% 0px" },
);
homeParts.current.map((part) => {
menuObserver.observe(part!);
});
}, []);
console.log("Home is rendered");
return (
<>
<div
className="hero"
id="home"
ref={(element) => homeParts.current.push(element)}
>
<section>
<div className="depth"></div>
<div className="heroContent">
<h1>This is Home</h1>
</div>
</section>
</div>
<div
id="projects"
style={{ height: "100vh", backgroundColor: "gray" }}
ref={(element) => homeParts.current.push(element)}
>
This is Projects
</div>
<div
id="contact"
style={{ height: "100vh", backgroundColor: "cadetblue" }}
ref={(element) => homeParts.current.push(element)}
>
This is contact
</div>
</>
);
};
Header
import linkSection from "../contexts/homeLinks";
function Header() {
const lastHash = useRef("");
const { homePart } = useContext(linkSection);
const homeLinks = [
{ href: "home", title: "Home" },
{ href: "projects", title: "Projects" },
{ href: "contact", title: "Contact" },
];
return (
<header>
<div>
<a href="/">Menu</a>
</div>
<nav>
<ul>
{homeLinks.map((homeLink) => (
<li key={homeLink.href}>
<a
href={"/#" + homeLink.href}
className={homePart == homeLink.href ? "active" : ""}
>
{homeLink.title}
</a>
</li>
))}
</ul>
</nav>
</header>
);
}
export default Header;
Context
import { createContext } from "react";
const homeLinks = createContext({
homePart: "",
setHomePart: (part: string) => {},
});
export default homeLinks;
Full code is also available in https://codesandbox.io/p/devbox/9p6fyv?file=%2Fsrc%2Fcontexts%2FhomeLinks.tsx&embed=1
Whenever the
IntersectionObserverdetects an intersection, it sets the state inApp, andApp, and all it's children are re-rendered:Instead the context should be a funnel to register to intersection events, without holding a state of it's own. If a component needs to set a state (and trigger a re-render) it should listen to an event, and set it's own state.
Sandbox
The
homeLinkcontextThe context exports 2 hooks:
useRegisterListener- accepts a function to register as listener.useObserve- returns anobservefunction that can be used as a function ref to observe a DOM elements.The context also exports
HomeLinksProviderthe initiates theIntersectionObserver, and thelistenersSet. It supplies the context consumer with all relevant functions to add/remove listener, and observe DOM elements. The provider handles all cleanups in case of unmount.It also waits for the observer to be ready before rendering the wrapped children.
Usage:
App:
Home:
Header: