Hide Show component and highlight link with react scroll - reactjs

Problem statement:
On clicking the react scroll link, the link is not highlighted(I've used spy) and it is not scrolling to the div instead just landing to the page.
Is there any other efficient way to do it? As i'm learning react by doing
Page Context component:
export const PageContext = createContext({
pageId: 'homepage',
scrollToPage: () => {}
})
There is a homepage component
const Home = () => {
const [pageId, setPageId] = useState('homepage');
const anotherCompRef = React.useRef();
const profileCompRef = React.useRef();
const scrollToPage = (event) => {
let pageId = event.target ? event.target.getAttribute('data-pageId') : null
if (pageId) {
setPageId(pageId);
Scroll.scroller.scrollTo(pageId, {
duration: 500,
delay: 100,
smooth: true,
spy: true,
exact: true,
offset: -80,
})
}
}
const renderer = () => {
switch (pageId) {
case 'profile':
return <ProfileView profileCompRef={profileCompRef} />
default:
return <AnotherView anotherCompRef={anotherCompRef}/>
}
}
return (
<>
<PageContext.Provider value={{pageId, scrollToPage: e => scrollToPage(e)}}>
<Layout>
{renderer()}
</Layout>
</PageContext.Provider>
</>
)
}
Layout component:
const Layout = ( {children} ) => {
return (
<>
<Header/>
<MainContainer children={children}/>
<Footer />
</>
)
}
export default Layout
Profileview component:
const ProfileView = (props) => {
return (
<>
<ProfileContainer id='profile' ref={props.profileCompRef} >
do stuff
</ProfileContainer>
</>
)
}
export default ProfileView
AnotherView component
const AnotherView = (props) => {
return (
<>
<AnotherViewContainer id='anotherView' ref={props.anotherCompRef} >
do stuff
</AnotherViewContainer>
</>
)
}
export default AnotherView
Header component:
const Header = () => {
const pageContext = useContext(PageContext)
return (
<>
<NavbarContainer>
<NavbarMenu>
<NavItem>
<NavLink to='profile' data-pageId='profile' smooth={true} duration={500} spy={true} exact='true' offset={-80} onClick={(e) => pageContext.scrollToPage(e)}>
Profile
</NavLink>
</NavItem>
<NavItem>
<NavLink to='anotherView' data-pageId='anotherView' smooth={true} duration={500} spy={true} exact='true' offset={-80} onClick={(e) => pageContext.scrollToPage(e)}>
Another View
</NavLink>
</NavItem>
</NavbarMenu>
</NavbarContainer>
</>
)
}
export default Header

I have mainly fixed the below mentioned issues.
Enable browser scroll by setting the page height more than the viewport height. Only then scrolling can happen.
Not suitable to add a click event to react-scroll Link. So I developed a custom link.
Also did some modifications in updating the pageId also.
Note - Below mentioned only about updated files.
Header.js
import { useContext } from "react";
import PageContext from "./PageContext";
const Header = () => {
const { pageId, setPageId } = useContext(PageContext);
const scrollTo = (e) => {
e.preventDefault();
setPageId(e.target.dataset.pageid);
};
return (
<>
<div>
<div>
<div>
<a
href="#profile"
data-pageid="profile"
onClick={scrollTo}
className={`${pageId === "profile" ? "active" : ""}`}
>
Profile
</a>
</div>
<div>
<a
href="#anotherview"
data-pageid="anotherview"
onClick={scrollTo}
className={`${pageId === "anotherview" ? "active" : ""}`}
>
Another View
</a>
</div>
</div>
</div>
</>
);
};
export default Header;
Home.js
import { useEffect, useRef, useState } from "react";
import { scroller } from "react-scroll";
import AnotherView from "./AnotherView";
import Layout from "./Layout";
import PageContext from "./PageContext";
import ProfileView from "./ProfileView";
const Home = () => {
const [pageId, setPageId] = useState("homepage");
const anotherCompRef = useRef();
const profileCompRef = useRef();
useEffect(() => {
if (pageId !== "homepage")
scroller.scrollTo(pageId, {
duration: 500,
delay: 100,
smooth: true
});
}, [pageId]);
const renderer = () => {
switch (pageId) {
case "profile":
return <ProfileView profileCompRef={profileCompRef} />;
default:
return <AnotherView anotherCompRef={anotherCompRef} />;
}
};
return (
<>
<PageContext.Provider value={{ pageId, setPageId }}>
<Layout>{renderer()}</Layout>
</PageContext.Provider>
</>
);
};
export default Home;
Layout.js
import { useContext } from "react";
import Header from "./Header";
import PageContext from "./PageContext";
const Layout = ({ children }) => {
const { pageId } = useContext(PageContext);
return (
<>
<Header />
<div id={pageId} children={children} />
</>
);
};
export default Layout;
styles.css
#root {
height: calc(100vh + 100px);
}
a.active {
color: red;
}
https://codesandbox.io/s/scrolling-with-react-scroll-68043895-bqt10?file=/src/Home.jsx
Let me know if you need further support.

Related

Re-Rendering component, after click event in different component

In my App I have a HeaderLogo component, with <h1> containing animation (inside its head-main class). I would like to re-render this component to trigger animation, after onclick event in <NavLink>.
<NavLink> is inside DropdownMenu, which is inside MainNavi.
HeaderLogo
const HeaderLogo = () => {
return (
<header>
<h1 className="head-main">learning curve</h1>
</header>
)
}
export default HeaderLogo
Dropdown Menu
import { MenuItemContentSchool } from "./sub-components/MenuItemContentSchool"
import { useState } from "react";
import { NavLink } from "react-router-dom";
const DropdownMenu2 = () => {
const [click, setClick] = useState("");
const handleClick = () => {
setClick("hide-menu");
}
return (
<div className={`dropdown-holder-us ${click}`}>
{MenuItemContentSchool.map((item) => {
return (
<NavLink
to={item.link}
className='d-content-us'
onClick={handleClick}
key={item.title}
>
{item.title}
</NavLink>
)
} )}
</div>
)
}
export default DropdownMenu2
App
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import HeaderLogo from "./components/HeaderLogo";
import NaviMain from "./components/NaviMain";
function App() {
return (
<Router>
<div className="App">
<HeaderLogo />
<NaviMain />
<Routes>
//...
</Routes>
</div>
</Router>
);
}
export default App;
NaviMain
import DropdownMenu2 from "./DropdownMenu2";
const NaviMain = () => {
return (
<nav>
<ul className="nav-main">
<li className="nav-main__button">
<a>school</a>
<DropdownMenu2 />
</li>
</ul>
</nav>
)
}
export default NaviMain
I do not know whether this will work or not but u can try the following solution:
set an id:
<h1 id="testing" className="head-main">learning curve</h1>
Change
const handleClick = () => {
setClick("hide-menu");
}
to
const handleClick = () => {
setClick("hide-menu");
let element = document.getElementById('#testing').
element.classList.remove("head-main");
element.classList.add("head-main");
}
Please let me know whether this solution works or not.

Dropdown menu - disappear after clicking on item

I'm trying to create "dropdown menu" in React. I've used useState inside functions in onMouseEnter and onMouseLeave events (in dropdown-holder-us div / NaviMain), to toggle display of "dropdown menu". Last thing is to make "dropdown menu" disappear , when clicking on DropdownMenuItem. Could someone hint me how to achieve this?
App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import NaviMain from "./components/NaviMain";
import Info from "./pages/Info";
import VerticalAlign from "./pages/VerticalAlign";
import Flexbox from "./pages/Flexbox";
function App() {
return (
<Router>
<div className="App">
<NaviMain />
<Routes>
<Route path="/" element={<Info />} />
<Route path="/verticalalign" element={<VerticalAlign />} />
<Route path="/flexbox" element={<Flexbox/>} />
</Routes>
</div>
</Router>
);
}
export default App;
NaviMain.js
import { useState } from "react"
import DropdownMenuItem from "./sub-components/DropdownMenuItem"
const NaviMain = () => {
const [disp, setDisp] = useState("hide-menu");
const hoverOn = () => {
setDisp("show-menu")
}
const hoverOff = () => {
setDisp("hide-menu")
}
return (
<nav>
<ul">
<li onMouseEnter={hoverOn} onMouseLeave={hoverOff}>
<a className="hover-pointer">school</a>
<div className={`dropdown-holder-us ${disp}`}>
<DropdownMenuItem title="v align" link={"/verticalAlign"}/>
<DropdownMenuItem title="flexbox" link={"/flexbox"} />
</div>
</li>
</ul>
</nav>
)
}
export default NaviMain
DropdownMenuItem.js
import { Link } from "react-router-dom";
const DropdownMenuItem = ({title , link}) => {
return (
<>
<Link to={link}">
{title}
</Link>
</>
)
}
export default DropdownMenuItem
I think easiest solution is to make disp a boolean which decides whether the dropdown menu is shown or not. I have shortened the dropdown menu to a <DropdownMenu> component for better legibility (you might want to do this in your code to, might be easier to maintain).
const NaviMain = () => {
const [display, setDisplay] = useState(false);
const hoverOn = () => {
setDisplay(true)
}
const hoverOff = () => {
setDisplay(false)
}
return (
<nav>
<ul">
<li onMouseEnter={hoverOn} onMouseLeave={hoverOff}>
<a className="hover-pointer">school</a>
{
display ? <DropdownMenu / >
}
</li>
</ul>
</nav>
)
}
If it is difficult to make dropdown menu appear and disappear like this,
another way to make it work is to attach disp to a css class which decides if DropdownMenu is display: block or display: none.
After rebuilding NaviMain.js (like #cSharp suggested in his answer), Ive created separate components DropdownMenu (containing logic) and MenuItemContentSchool (with content). So the "hover" effect, managed by mouse evenets, is in li - which is a parent for "DropdownMenu". And the desired disappearance of "DropdownMenu" is managed by click event, inside the container div. Quite simple, inspired by Brian Design video.
NaviMain.js
import { useState } from "react"
import DropdownMenu from "./DropdownMenu";
const NaviMain = () => {
const [disp, setDisp] = useState(false);
const hoverOn = () => setDisp(true)
const hoverOff = () => setDisp(false)
return (
<nav>
<ul>
<li className= onMouseEnter={hoverOn} onMouseLeave={hoverOff}>
<a>school</a>
{ disp && <DropdownMenu /> }
</li>
</ul>
</nav>
)
}
export default NaviMain
DropdownMenu
import { useState } from "react"
import { MenuItemContentSchool } from "./sub-components/MenuItemContentSchool"
import { Link } from "react-router-dom";
const DropdownMenu = ( {disp} ) => {
const [click, setClick] = useState("")
const handleClick = () => {
setClick("hide-menu")
}
return (
<div className={`dropdown-holder-us ${disp} ${click}`}>
{MenuItemContentSchool.map((item, index) => {
return (
<Link to={item.link} className="d-content-us" onClick={handleClick}>
{item.title}
</Link>
)
} )}
</div>
)
}
export default DropdownMenu
MenuItemContentSchool
export const MenuItemContentSchool = [
{
title:"v align",
link:"/verticalAlign"
},
{
title:"flexbox",
link:"/flexbox"
}
]

GSAP cursor animation makes me lose focus on nav bar

I am experimenting the GSAP tutorial to understand how it works with React. I can't figure out why I cannot click on my navigation link anymore when the following code has been implemented in my home page :
My Circle Component
const Circle = forwardRef(({ size, delay }, ref) => {
const el = useRef();
useImperativeHandle(
ref,
() => {
return {
moveTo(x, y) {
gsap.to(el.current, { x, y, delay });
},
};
},
[delay]
);
return <div className={`circle ${size}`} ref={el}></div>;
});
(extract of) My Homepage
import { gsap } from "gsap";
import { useEffect, useRef } from "react";
import { Link } from "react-router-dom";
import Circle from "../../components/circle/Circle";
const Home = () => {
//special cursor
const circleRefs = useRef([]);
useEffect(() => {
circleRefs.current.forEach((ref) =>
ref.moveTo(window.innerWidth / 2, window.innerHeight / 2)
);
const onMove = ({ clientX, clientY }) => {
circleRefs.current.forEach((ref) => ref.moveTo(clientX, clientY));
};
window.addEventListener("pointermove", onMove);
return () => window.removeEventListener("pointermove", onMove);
}, []);
const addCircleRef = (ref) => {
if (ref) circleRefs.current.push(ref);
};
return (
<div className="home">
<div className="topbar">
<nav>
<ul>
<Link to={`/characters`}>
<li>Characters</li>
</Link>
<Link to={`/comics`}>
<li>Comics</li>
</Link>
<Link to={`/favorites`}>
<li>Favorites</li>
</Link>
</ul>
</nav>
</div>
<Circle size="sm" ref={addCircleRef} delay={0} />
<Circle size="md" ref={addCircleRef} delay={0.1} />
<Circle size="lg" ref={addCircleRef} delay={0.2} />
</div>
);
};
export default Home;
Is it because ref is placed on the cursor ?

document.querySelector() always return null when clicking on React Router Link the first time but will return correctly after

I'm stucking on a problem with React-Router and querySelector.
I have a Navbar component which contains all the CustomLink components for navigation and a line animation which listens to those components and displays animation according to the current active component.
// Navbar.tsx
import React, { useCallback, useEffect, useState, useRef } from "react";
import { Link, useLocation } from "react-router-dom";
import CustomLink from "./Link";
const Layout: React.FC = ({ children }) => {
const location = useLocation();
const navbarRef = useRef<HTMLDivElement>(null);
const [pos, setPos] = useState({ left: 0, width: 0 });
const handleActiveLine = useCallback((): void => {
if (navbarRef && navbarRef.current) {
const activeNavbarLink = navbarRef.current.querySelector<HTMLElement>(
".tdp-navbar__item.active"
);
console.log(activeNavbarLink);
if (activeNavbarLink) {
setPos({
left: activeNavbarLink.offsetLeft,
width: activeNavbarLink.offsetWidth,
});
}
}
}, []);
useEffect(() => {
handleActiveLine();
}, [location]);
return (
<>
<div className="tdp-navbar-content shadow">
<div ref={navbarRef} className="tdp-navbar">
<div className="tdp-navbar__left">
<p>Todo+</p>
<CustomLink to="/">About</CustomLink>
<CustomLink to="/login">Login</CustomLink>
</div>
<div className="tdp-navbar__right">
<button className="tdp-button tdp-button--primary tdp-button--border">
<div className="tdp-button__content">
<Link to="/register">Register</Link>
</div>
</button>
<button className="tdp-button tdp-button--primary tdp-button--default">
<div className="tdp-button__content">
<Link to="/login">Login</Link>
</div>
</button>
</div>
<div
className="tdp-navbar__line"
style={{ left: pos.left, width: pos.width }}
/>
</div>
</div>
<main className="page">{children}</main>
</>
);
};
export default Layout;
// CustomLink.tsx
import React, { useEffect, useState } from "react";
import { useLocation, useHistory } from "react-router-dom";
interface Props {
to: string;
}
const CustomLink: React.FC<Props> = ({ to, children }) => {
const location = useLocation();
const history = useHistory();
const [active, setActive] = useState(false);
useEffect(() => {
if (location.pathname === to) {
setActive(true);
} else {
setActive(false);
}
}, [location, to]);
return (
// eslint-disable-next-line react/button-has-type
<button
className={`tdp-navbar__item ${active ? "active" : ""}`}
onClick={(): void => {
history.push(to);
}}
>
{children}
</button>
);
};
export default CustomLink;
But it doesn't work as I want. So I opened Chrome Devtool and debugged, I realized that when I clicked on a CustomLink first, the querySelector() from Navbar would return null. But if I clicked on the same CustomLink multiple times, it would return properly, like the screenshot below:
Error from Chrome Console
How can I get the correct return from querySelector() from the first time? Thank you!
It's because handleActiveLine will trigger before setActive(true) of CustomLink.tsx
Add a callback in CustomLink.tsx:
const CustomLink: React.FC<Props> = ({ onActive }) => {
useEffect(() => {
if (active) {
onActive();
}
}, [active]);
}
In Navbar.tsx:
const Layout: React.FC = ({ children }) => {
function handleOnActive() {
// do your query selector here
}
// add onActive={handleOnActive} to each CustomLink
return <CustomLink onActive={handleOnActive} />
}

Conditional className not updated when boolean updated

I know this should be stupid simple. All of the examples I can find on here are stupid simple. But for some reason I can't get a className to change conditionally in my component.
I'm trying to animate the site logo when the user scrolls down. fullLogo is changing as expected according to the logs. But the class never changes. The only thing I can think is that once the render happens that's it, and it needs to be declared as a class in order to re-render like that. Is that the issue?
const headerHeight = 89
let fullLogo = true
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > headerHeight) {
console.log('scrolled beyond header height')
fullLogo = false;
} else {
fullLogo = true;
}
console.log('fullLogo', fullLogo)
})
<header className={(!!fullLogo ? 'full-logo' : '')}>
Here's the full component if that helps:
import React, { useState } from "react"
import PropTypes from "prop-types"
import {
Collapse,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem,
NavLink
} from 'reactstrap'
import "./header.scss"
const Header = ({ siteTitle, menu, location }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
const headerHeight = 89
let fullLogo = true
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > headerHeight) {
console.log('scrolled beyond header height')
fullLogo = false;
} else {
fullLogo = true;
}
console.log('fullLogo', fullLogo)
})
menu.forEach((item, index) => {
if (item.path === location.pathname) {
item.active = true
} else {
item.active = false
}
})
return (
<header className={(!!fullLogo ? 'full-logo' : '')}>
<Navbar color="light" light expand="md" fixed="top">
<NavbarBrand href="/">
<span className="initial">L</span>
<span className="rest">OGO</span>
<span className="splitter"></span>
<span className="initial">N</span>
<span className="rest">AME</span>
</NavbarBrand>
<NavbarToggler onClick={toggle} />
<Collapse isOpen={isOpen} navbar>
<Nav className="mr-auto" navbar>
{menu.map((item, index) => (
<NavItem key={`nav_link_${index}`}>
<NavLink href={item.path} className={item.active ? `active` : null}>{item.title}</NavLink>
</NavItem>
))}
</Nav>
</Collapse>
</Navbar>
</header>
)
}
Header.propTypes = {
siteTitle: PropTypes.string,
menu: PropTypes.array
}
Header.defaultProps = {
siteTitle: ``,
menu: {
title: `Home`,
path: `/`
}
}
export default Header
The code by using hooks be like
import React, { useState } from "react"
import PropTypes from "prop-types"
import {
Collapse,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem,
NavLink
} from 'reactstrap'
import "./header.scss"
const Header = ({ siteTitle, menu, location }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
const headerHeight = 89
const [fullLogo,setFullLogo] = useState(true)
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > headerHeight) {
console.log('scrolled beyond header height')
setFullLogo(false);
} else {
setFullLogo(true);
}
console.log('fullLogo', fullLogo)
})
menu.forEach((item, index) => {
if (item.path === location.pathname) {
item.active = true
} else {
item.active = false
}
})
return (
<header className={(!fullLogo ? 'full-logo' : '')}> //If You Want to toggle current state
<Navbar color="light" light expand="md" fixed="top">
<NavbarBrand href="/">
<span className="initial">L</span>
<span className="rest">OGO</span>
<span className="splitter"></span>
<span className="initial">N</span>
<span className="rest">AME</span>
</NavbarBrand>
<NavbarToggler onClick={toggle} />
<Collapse isOpen={isOpen} navbar>
<Nav className="mr-auto" navbar>
{menu.map((item, index) => (
<NavItem key={`nav_link_${index}`}>
<NavLink href={item.path} className={item.active ? `active` : null}>{item.title}</NavLink>
</NavItem>
))}
</Nav>
</Collapse>
</Navbar>
</header>
)
}
Header.propTypes = {
siteTitle: PropTypes.string,
menu: PropTypes.array
}
Header.defaultProps = {
siteTitle: ``,
menu: {
title: `Home`,
path: `/`
}
}
export default Header
Your assumption is correct. There's no lifecycle event that's triggering a re-render. Functional components only update if you change props or use hooks. Here you could use a hook to setDisplayFullLogo.

Resources