Offset Top in React Functional Components, getBoundingClientRect not working properly - reactjs

I'm trying to implement class-swapping site navigation using React Functional components during the scroll screen.
I added a listener to windows and I'm using the getBoundingClientRect method to check the moment my top arrives in a position to change its class.
The code below always returns the Top equal to zero, regardless of the position of my scroll.
Where am I going wrong in this example?
import React, {useEffect, useRef} from "react";
const Navigation = () => {
const inputRef = useRef();
const sections = [{
name: "Portfolio",
url: "#portfolio"
},
{
name: "About",
url: "#about"
},
{
name: "Contact",
url: "#contact"
}];
const navLinks = sections.map((section, index) => {
return (
<li className="nav-item mx-0 mx-lg-1" key={index}>
<a className="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger" href={section.url}>{section.name}</a>
</li>
)
});
const handleScroll = () => {
let offsetTop = inputRef.current.getBoundingClientRect().top;
console.log('Top ' + offsetTop);
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
});
return (
<nav className="navbar navbar-expand-lg bg-secondary text-uppercase fixed-top" id="mainNav" ref={ inputRef }>
<div className="container">
<a className="navbar-brand js-scroll-trigger" href="#page-top">Start Bootstrap</a>
<button className="navbar-toggler navbar-toggler-right text-uppercase font-weight-bold bg-primary text-white rounded" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu
<i className="fas fa-bars"></i>
</button>
<div className="collapse navbar-collapse" id="navbarResponsive">
<ul className="navbar-nav ml-auto">
{navLinks}
</ul>
</div>
</div>
</nav>
);
};
export default Navigation;
When scrolling the top, right, bottom, left, heigh and width positions are the same.
I expect that the top value changes when scrolling the page.
My code is working now, follow the final version
import React, { useEffect, useRef, useState } from "react";
import { Link, animateScroll as scroll } from "react-scroll";
const Navigation = () => {
const inputRef = useRef();
const [navClass, setNavClass] = useState('');
const sections = [{
name: "Portfolio",
url: "portfolio"
},
{
name: "About",
url: "about"
},
{
name: "Contact",
url: "contact"
}];
const navLinks = sections.map((section, index) => {
return (
<li className="nav-item mx-0 mx-lg-1" key={index}>
<Link
activeClass="active"
to={section.url}
spy={true}
smooth="easeInOutQuart"
offset={-70}
duration={800}
className="nav-link py-3 px-0 px-lg-3 rounded"
href="">
{section.name}
</Link>
</li>
)
});
const scrollToTop = () => {
scroll.scrollToTop();
};
const handleScroll = () => {
let offsetTop = window.pageYOffset;
if ( offsetTop > 100 ){
setNavClass('navbar-shrink');
}else{
setNavClass('');
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
});
return (
<nav className={`navbar navbar-expand-lg bg-secondary text-uppercase fixed-top ${navClass}`} id="mainNav" ref={inputRef}>
<div className="container">
<a className="navbar-brand js-scroll-trigger" href="#page-top" onClick={scrollToTop}>Start Bootstrap</a>
<button className="navbar-toggler navbar-toggler-right text-uppercase font-weight-bold bg-primary text-white rounded" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu
<i className="fas fa-bars"></i>
</button>
<div className="collapse navbar-collapse" id="navbarResponsive">
<ul className="navbar-nav ml-auto">
{navLinks}
</ul>
</div>
</div>
</nav>
);
};
export default Navigation;

If your objective is to change the class of your navbar based on the scroll position of your window, it would make more sense to use window.pageYOffset instead. Consider that the navbar would never actually leave its position, so whenever you call .getBoundingClientRect().top, it will always be 0.
const handleScroll = () => {
let offsetTop = window.pageYOffset;
console.log('Top ' + offsetTop);
};
Check out this sandbox on how you can change the appearance of your navbar on scroll: https://codesandbox.io/s/navbar-change-color-onscroll-jepyc

Related

I want to re-render the component when state change inside async function

import React, { useState, useEffect, useCallback } from "react";
// import "./SideNav.css";
// import { _removeAllLs, _getSecureLs } from "../../helper/storage";
import {
NavLink,
// Routes,
// useLocation,
// useNavigate,
// useParams,
// useRoutes,
} from "react-router-dom";
import { getSeekerProfile } from "../../services/seeker";
import { _getSecureLs } from "../../helper/storage";
// import className from "classnames";
function SideNav() {
// const navigate = useNavigate();
const [seekerProfilePath, setSeekerProfilePath] = useState(null);
const { user } = _getSecureLs("seekerAuth");
// const userMode = _getSecureLs("auth")?.mode;
// console.log(userMode);
const getSeeker = useCallback(async () => {
try {
const response = await getSeekerProfile(user?.id);
console.log(response);
setSeekerProfilePath(response?.imgPath);
// navigate("/account/seeker/upload_photo");
} catch (e) {
throw new Error(e);
}
}, [user?.id]);
useEffect(() => {
getSeeker();
}, [getSeeker]);
console.log(seekerProfilePath);
return (
<aside className="main-sidebar sidebar-dark-primary ">
<div className="ourteam__image__wrapper">
<img
src={
seekerProfilePath
? `http://localhost:8000/${seekerProfilePath}`
: require("../../Assets/Images/user.png")
}
alt="rohan-pic"
/>
</div>
<div className="sidebar ">
<nav className="mt-2 mb-2 d-flex flex-column justify-content-between align-items-between">
<ul className="nav nav-pills nav-sidebar flex-column" role="menu">
<li className="nav-item">
<NavLink
exact
to="dashboard"
className={({ isActive }) =>
isActive ? "nav-link active" : "nav-link"
}
>
<i className="nav-icon fa fa-house-user" aria-hidden="true"></i>
Dashboard
</NavLink>
</li>
<li className="nav-item">
<NavLink
exact
to="upload_photo"
className={({ isActive }) =>
isActive ? "nav-link active" : "nav-link"
}
>
<i className="nav-icon fa fa-camera" aria-hidden="true"></i>
Upload A Photo
</NavLink>
</li>
<li className="nav-item">
<NavLink
exact
to="cv_cover_letter"
className={({ isActive }) =>
isActive ? "nav-link active" : "nav-link"
}
>
<i className="nav-icon fa fa-bullhorn" aria-hidden="true"></i>
CV & Cover Letter
</NavLink>
</li>
<li className="nav-item">
<NavLink
exact
to="change_password"
className={({ isActive }) =>
isActive ? "nav-link active" : "nav-link"
}
>
<i className="nav-icon fa fa-key" aria-hidden="true"></i>
Change Password
</NavLink>
</li>
</ul>
</nav>
{/* <div class="sidebar-custom">
<button
onClick={() => {
_removeAllLs();
navigate(ROUTES.LOGIN);
}}
class="mt-2 btn btn-secondary hide-on-collapse pos-right"
>
<i class="fas fa-sign-out-alt"></i> Logout
</button>
</div> */}
</div>
</aside>
);
}
export default SideNav;
I want to reflect immediatley reflect the uploaded image in the component when the user upload the images
but the code is not doing what i expected. I have to manaually refresh the component to see the uploaded image. I have used usecallback and useEffect in the component. Can't we use state as a dependency array in the useEffect hook.

Nextjs navbar active class only becomes active on a second click

I'm trying to create header component for next.js/tailwindcss app. The nav active class only shows active when clicked on a second time. I'd like for it to be active upon the first click. Where am I going wrong? What element should I target with tailwindcss to reflect active state?
navlink.js file:
import Link from 'next/link';
const NavItem = ({ text, href, active }) => {
return (
<Link href={href}>
<a
className={`nav__item ${
active ? 'active underline underline-offset-8' : ''
}`}
>
{text}
</a>
</Link>
);
};
export default NavItem;
header.js :
import Image from 'next/image';
import React, { useState } from 'react';
import NavItem from './NavItem';
const MENU_LIST = [
{ text: 'About', href: '/about' },
{ text: 'My Work', href: '/MyWork' },
{ text: 'Blog', href: '/blog' },
{ text: 'Contact', href: '/contact' },
];
const Header = () => {
const [navActive, setNavActive] = useState(null);
const [activeIdx, setActiveIdx] = useState(-1);
return (
<header className="bg-white">
<nav className="max-w-5xl mx-auto border border-top-gray">
{/*containment div*/}
<div className="flex justify-between">
{/*Logo Container*/}
<div className="cursor-pointer">
<a href="/">
<Image
src="/../public/images/soulLogo.webp"
alt="site logo"
width={233}
height={144}
/>
</a>
</div>
{/*Link Container*/}
<div className="hidden md:flex">
<div
onClick={() => setNavActive(!navActive)}
className={`nav__menu-bar md:underline underline-offset-8 decoration-black `}
></div>
<div
className={`${
navActive
? 'active underline underline-offset-8 decoration-black'
: ''
} nav__menu-list flex items-center space-x-8 text-gray-700 tracking-wider `}
>
{MENU_LIST.map((menu, idx) => (
<div
onClick={() => {
setActiveIdx(idx);
setNavActive(false);
}}
key={menu.text}
>
<NavItem active={activeIdx === idx} {...menu} />
</div>
))}
</div>
</div>
{/*Mobile Menu button*/}
<div className="flex relative flex-col gap-y-2 cursor-pointer pt-14 pr-3 md:hidden">
<div className="w-24 h-1 bg-black shadow-gray-700 rounded"></div>
<div className="w-24 h-1 bg-black shadow-gray-700 rounded"></div>
<div className="w-24 h-1 bg-black shadow-gray-700 rounded"></div>
</div>
{/*Mobile Menu*/}
<
</div>
</nav>
</header>
);
};
export default Header;
I think we used the same code and I ran into the same problem.
I fixed it with with using next/router and comparing the paths inside the NavItem.js component.
navitem.js
import Link from "next/link";
import { useRouter } from 'next/router';
const NavItem = ({ href, text }) => {
const router = useRouter();
const currentRoute = router.pathname;
return (
<Link href={href} className={currentRoute === `${href}` ? 'active' : ''}> {text} </Link>
);
};
export default NavItem;
navbar.js
{PRIMARY_NAVIGATION_LIST.map((menu) => (
<div key={menu.text} >
<NavItem
href={menu.href}
text={menu.text}
/>
</div>
))}

How to display username on navbar after login in react redux

There is a nav link on my navbar named Login (I have used bootstrap navbar). After successful login I want to replace Login with a username using react-redux (not redux-toolkit).
Kindly help me in writing reducer and action code. As I am a beginner in redux and I don't know how to code in reducer and action file it is very confusing for me.
Navbar component (Navbar.js)
import React from 'react';
import { Link } from 'react-router-dom';
import { useSelector } from 'react-redux';
const Navbar = () => {
const {username} = useSelector((state) => state.user);
return (
<div>
<nav className="navbar navbar-expand-lg">
<div className="container-fluid">
<Link className="navbar-brand" to="/hommepage">Ebuy</Link>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria- expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon">
<i className="fas fa-bars" style={{color: 'white', fontSize: '28px'}}></i>
</span>
</button>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className="nav-item">
<Link className="nav-link" aria-current="page" to="/homepage">Home</Link>
</li>
<li className="nav-item dropdown">
<Link className="nav-link dropdown-toggle" to="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Select Category
</Link>
<ul className="dropdown-menu" aria-labelledby="navbarDropdown">
<li><Link className="dropdown-item" to="/skincare">Skincare Products</Link></li>
<li><Link className="dropdown-item" to="/stationary">Stationary Products</Link></li>
<li><Link className="dropdown-item" to="#">Clothing</Link></li>
</ul>
</li>
</ul>
<div className="d-flex">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className='nav-item'>
<Link className="nav-link" to="/">Login</Link>
</li>
</ul>
</div>
</div>
</div>
</nav>
</div>
);
};
export default Navbar;
reducer
const initialState = {
username: null,
}
export const userReducer(state = initialState, action) {
switch (action.type) {
case "CHANGE_USERNAME":
return {
...state,
username: action.payload,
}
default:
return state
}
}
action
export const changeUserName = (name) => {
return {
type: "CHANGE_USERNAME",
payload: name
}
}
after login call this
const dispatch = useDispatch()
const login = () => {
... login logic here
dispatch(changeUserName("user-1"))
}
in navbar
const { username } = useSelector(state=> state.user)
In your Navbar Component you should get username from redux, please try this :
const {username} = useSelector(state=> state.yourReducerName)
after this, in your jsx : you should write a condition like this :
{username ? (<div>{username}</div>): ()<span>Login</span>}
EDIT :
Please refer to this sandbox example and take a look on the reducer and actions files, also on the navbar components.
Let us know if the post helped you :)

NextJS and bootstrap mobile nav

I'm using bootstrap with NextJS for my navbar but have one issue because of the dynamic page changes with React and Next on mobile view the menu stays open after clicking a link and loading new page.
Tried a few ways to try and make it close on page changes but been unsuccessful. What would be best way to achieve this?
import { useEffect } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { ArrowDownCircle } from 'react-bootstrap-icons'
import logo from '../public/images/logo.gif'
import styles from '../styles/Navbar.module.scss'
const Navbar = () => {
useEffect(() => {
import('bootstrap/js/dist/collapse')
import('bootstrap/js/dist/dropdown')
}, []);
return (
<nav id="navbar" className="navbar navbar-expand-md navbar-light container">
<div>
<Link href="/" prefetch={false}><a className="navbar-brand">
<Image
src={logo}
alt="Logo"
layout="intrinsic"
width={200}
height={50}
priority={true}
/>
</a></Link>
</div>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarNav">
<ul className={`${styles.navLink} navbar-nav ms-auto`}>
<li className="nav-item">
<Link href="/" prefetch={false}><a className={`${styles.link} nav-link`} aria-current="page">Home</a></Link>
</li>
<li className="nav-item">
<Link href="/about" prefetch={false}><a className={`${styles.link} nav-link`}>About</a></Link>
</li>
<li className="nav-item dropdown">
<a className={`${styles.link} nav-link`} id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">Services <ArrowDownCircle className="ms-1" /></a>
<ul className="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<li><Link href="/web-design"><a className="dropdown-item">Web Design</a></Link></li>
<li><Link href="/web-development"><a className="dropdown-item">Web Development</a></Link></li>
<li><Link href="/ecommerce"><a className="dropdown-item">eCommerce</a></Link></li>
</ul>
</li>
<li className="nav-item">
<Link href="/faq" prefetch={false}><a className={`${styles.link} nav-link`}>FAQ</a></Link>
</li>
<li className="nav-item">
<Link href="/contact" prefetch={false}><a className={`${styles.link} nav-link`}>Contact</a></Link>
</li>
</ul>
</div>
<style jsx>{`
a {
background: none;
}
a:hover {
background: none;
}
`}</style>
</nav>
);
}
export default Navbar;
If anyone still looking for solution in 2022 or later, follow these steps:
Note: This solution was tested on NextJs (Typescript) + Bootstrap v.5.2
Add an id to the button with class navbar-toggler i.e. <button id="navbar-toggler" className="navbar-toggler" ...
In your component class, say Header.tsx
Import useRouter and useEffect:
import { useRouter } from 'next/router'
import { useEffect } from 'react'
and within your component function:
const Header = () => {
const router = useRouter()
useEffect(() => {
const handleRouteChange = (url: String) => {
if (typeof window !== undefined && window.screen.width <= 991.98) { // change the width value according to your navbar breakpoint
const navbar = document.getElementById("navbar-toggler");
if (navbar !== null && !navbar.classList.contains('collapsed')) navbar.click()
}
}
router.events.on('routeChangeComplete', handleRouteChange)
}, [router.events])
return (<> ... other codes </>)
}
export default Header

How to use Jest for testing a react component with localStorage?

I have a component that calls to local storage and want to test it with jestJS. As far as I can tell jest does not support calls to localStorage.
This is the component that I need to have tests for:
const NavBar: React.FC = () => {
const history = useHistory();
const handleCLick = () => {
localStorage.clear();
history.push('/login');
};
return (
<div>
<header>
<div className="banner">
<div className="container">
<img
className="icon "
alt="icon"
title="icon"
src={favicon57}
/>
<p>Official website of the Stuff</p>
</div>
</div>
<nav className="navbar navbar-expand-md navbar-dark fixed-top">
<div className="container">
<div className="navbar-header">
<img
className="logo "
alt="logo"
title="Logo"
src={Blah}
/>
</div>
<button
className="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarCollapse"
aria-controls="navbarCollapse"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon" />
</button>
<div className="collapse navbar-collapse" id="navbarCollapse">
<ul className="navbar-nav ml-auto">
{isTokenAdmin() ? (
<li className="nav-item">
<a id="nav-users" className="nav-link" href={ADMIN_URL}>
View Users
</a>
</li>
) : (
<div> </div>
)}
{isTokenActive() ? (
<li className="nav-item">
<a id="nav-log-out" className="nav-link" href={APP_URL}>
Locations
</a>
</li>
) : (
<div> </div>
)}
{isTokenActive() ? (
<li className="nav-item">
<a
id="nav-log-out"
className="nav-link"
href={LOGIN_URL}
onClick={() => {
handleCLick();
}}
>
Logout
</a>
</li>
) : (
<div> </div>
)}
</ul>
</div>
</div>
</nav>
</header>
</div>
);
};
export default NavBar;
As you can see I am rendering the buttons based off of the token that I have stored in localStorage. How would you get this to 100% test coverage?
EDIT:
The code for the functions to get the token are:
export const isTokenActive = (): boolean => {
const userToken: string | null = localStorage.getItem('exp');
if (typeof userToken === 'string') {
return new Date().getTime() < Number.parseInt(userToken, 10);
}
return false;
};
export const isTokenAdmin = (): boolean => {
const userToken: string | null = localStorage.getItem('access_token');
if (typeof userToken === 'string') {
const decodedToken: TokenDetails = jwt_decode(userToken);
return decodedToken.authorities[0] === 'ROLE_Administrator';
}
return false;
};
You are correct that Jest does not support calls to localStorage. It is not a browser and doesn't implement localStorage.
The solution is to mock your own fake localStorage support like below:
browserMocks.js
const localStorageMock = (function() {
let store = {}
return {
getItem: function(key) {
return store[key] || null
},
setItem: function(key, value) {
store[key] = value.toString()
},
removeItem: function(key) {
delete store[key]
},
clear: function() {
store = {}
}
}
})()
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
})
Jest config (can be inside your package.json)
"jest": {
"setupFiles": [
"<rootDir>/__jest__/browserMocks.js",
Then to see if localstorage has been called you can spy on it like this:
describe('signOutUser', () => {
it('should sign out a user', async () => {
const spyLoStoRemove = jest.spyOn(localStorage, 'removeItem')
await signOutUser()
expect(spyLoStoRemove).toHaveBeenCalled()
expect(spyLoStoRemove).toHaveBeenCalledTimes(2)
})
})

Resources