GSAP cursor animation makes me lose focus on nav bar - reactjs

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 ?

Related

React Outside Hook not reacting to outside Click

I am finishing up a project where I want to use a small dropdown menu when I click on my settings icon. The problem is that for some reason it is not recognized when I click outside of that dropdown menu. I used a hook that worked in the same project with a different dropdown menu, but now it doesn't. Maybe because it is in a modal? I really don't know.
Here is the Repo of this Project: https://github.com/Clytax/fem-kanban
The Hook (I modified it a bit by excluding the elipsis icon so it doesnt reopen when Click on it to close it.)
import React from "react";
export const useOutsideClick = (callback, exclude) => {
const ref = React.useRef();
React.useEffect(() => {
const handleClick = (e) => {
if (
ref.current &&
!ref.current.contains(e.target) &&
!exclude.current.contains(e.target)
) {
callback();
}
};
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
}, [callback, exclude]);
return ref;
};
The Modal:
import React, { useRef, useState, useEffect } from "react";
import "./taskModal.scss";
import { ReactComponent as Elipsis } from "../../../assets/Icons/icon-vertical-ellipsis.svg";
import { ReactComponent as Close } from "../../../assets/Icons/icon-chevron-up.svg";
import { ReactComponent as Open } from "../../../assets/Icons/icon-chevron-down.svg";
import { useSelector, useDispatch } from "react-redux";
import modalSlice, {
closeViewTaskModal,
openEditTaskModal,
closeAllModals,
openDeleteTaskModal,
} from "../../../features/global/modalSlice";
import Backdrop from "../Backdrop/Backdrop";
import Subtask from "../../Task/Subtask";
import "../../Extra/DropdownSettings.scss";
import { useOutsideClick } from "../../../hooks/useOutsideClick";
import { motion } from "framer-motion";
import DropdownStatus from "../../Extra/DropdownStatus";
import DropdownSettings from "../../Extra/DropdownSettings";
import DropdownSettingsTask from "../../Extra/DropdownSettingsTask";
const ViewTaskModal = ({ handleClose }) => {
const [openSettings, setOpenSettings] = useState(false);
const dispatch = useDispatch();
const task = useSelector((state) => state.modal.viewTaskModal.task);
const handleCloseSettings = () => {
console.log("hi");
setOpenSettings(false);
};
const modal = useSelector((state) => state.modal);
const viewTaskModal = useSelector((state) => state.modal.viewTaskModal);
const elipsisRef = useRef(null);
const wrapperRef = useOutsideClick(handleCloseSettings, elipsisRef);
const getFinishedSubTasks = () => {
let finishedSubTasks = 0;
task.subTasks.forEach((subtask) => {
if (subtask.isDone) {
finishedSubTasks++;
}
});
return finishedSubTasks;
};
const closeModal = () => {
dispatch(closeViewTaskModal());
};
return (
<Backdrop onClick={closeModal} mobile={false}>
<motion.div
onClick={(e) => {
e.stopPropagation();
}}
onMouseDown={(e) => {
e.stopPropagation();
}}
className="view-task"
>
<div className="view-task__header | flex">
<h2 className="view-task__header__title">{task.name}</h2>
<div className="view-tastk__settings">
<div
className="view-task__header__icon"
style={{ cursor: "pointer" }}
ref={elipsisRef}
onClick={() => {
setOpenSettings(!openSettings);
}}
>
<Elipsis />
</div>
{openSettings && (
<div className="dropdown-settings__task" ref={wrapperRef}>
<div
className="dropdown-settings__item"
onClick={() => {
dispatch(closeAllModals());
dispatch(openEditTaskModal(task));
}}
>
Edit Task
</div>
<div
className="dropdown-settings__item"
onClick={() => {
dispatch(closeAllModals());
dispatch(openDeleteTaskModal(task));
}}
>
Delete Task
</div>
</div>
)}
</div>
</div>
<p className="view-task__description">{task.description}</p>
<div className="view-task__subtasks">
<p>
Subtasks ({getFinishedSubTasks()} of {task.subTasks.length})
</p>
<div className="view-task__subtasks__list">
{task.subTasks.map((subtask, index) => (
<Subtask
subtaskID={subtask.id}
boardID={task.boardID}
taskID={task.id}
columnID={task.columnID}
key={index}
/>
))}
</div>
</div>
<div className="view-task__status">
<p>Current Status</p>
<DropdownStatus click={handleCloseSettings} task={task} />
</div>
</motion.div>
</Backdrop>
);
};
export default ViewTaskModal;

React - pass context to SweetAlert popup

My context is as follows:
import React, {createContext, useEffect, useState} from "react";
export const CartContext = createContext();
const CartContextProvider = (props) => {
const [cart, setCart] = useState(JSON.parse(localStorage.getItem('cart')) || []);
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
const updateCart = (productId, op) => {
let updatedCart = [...cart];
if (updatedCart.find(item => item.id === productId)) {
let objIndex = updatedCart.findIndex((item => item.id === productId));
if (op === '-' && updatedCart[objIndex].qty > 1) {
updatedCart[objIndex].qty -= 1;
} else if (op === '+') {
updatedCart[objIndex].qty += 1;
}
} else {
updatedCart.push({id: productId, qty: 1})
}
setCart(updatedCart);
}
const removeItem = (id) => {
setCart(cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{cart, updateCart, removeItem}}>
{props.children}
</CartContext.Provider>
)
};
export default CartContextProvider;
App.js:
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import NavigationBar from "./components/layout/navigationBar/NavigationBar";
import Homepage from "./pages/homepage/Homepage";
import AboutUsPage from "./pages/aboutUs/AboutUsPage";
import ContactPage from "./pages/contact/ContactPage";
import SearchPage from "./pages/search/SearchPage";
import ShoppingCart from "./components/layout/shoppingCart/ShoppingCart";
import CartContextProvider from "./context/CartContext";
function App() {
return (
<div>
<CartContextProvider>
<Router>
<NavigationBar/>
<ShoppingCart/>
<Routes>
<Route exact path="/" element={<Homepage/>}/>
<Route path="/a-propos" element={<AboutUsPage/>} />
<Route path="/contact" element={<ContactPage/>}/>
<Route path="/recherche" element={<SearchPage/>}/>
</Routes>
</Router>
</CartContextProvider>
</div>
);
}
export default App;
In the component ShoppingCart I am using another component ShoppingCartQuantity which in turn makes use of the context. It works as it should.
Here's the ShoppingCartQuantity component:
import React, {useContext} from "react";
import {CartContext} from "../../../context/CartContext";
import styles from './ShoppingCartQuantity.module.css'
const ShoppingCartQuantity = ({productId}) => {
const {cart, updateCart} = useContext(CartContext);
let qty = 0;
if (cart.find((item => item.id === productId))) {
let objIndex = cart.findIndex((item => item.id === productId));
qty = cart[objIndex].qty;
}
return (
<div>
<span>
<span className={`${styles.op} ${styles.decrementBtn}`} onClick={() => updateCart(productId, '-')}>-</span>
<span className={styles.qty}>{qty}</span>
<span className={`${styles.op} ${styles.incrementBtn}`} onClick={() => updateCart(productId, '+')}>+</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
Now I am trying to use the ShoppingCartQuantity component in the Homepage component which is a route element (refer to App.js) but getting the error Uncaught TypeError: Cannot destructure property 'cart' of '(0 , react__WEBPACK_IMPORTED_MODULE_0__.useContext)(...)' as it is undefined.
So the context is working for components outside the router but not for those inside it. If I have wrapped the router within the provider, shouldn't all the route elements get access to the context or am I missing something?
UPDATE
As user Build Though suggested in the comments, I tried using the ShoppingCartQuantity component in another route element and it works fine; so the problem is not with the router!
Below is the code of how I am using the ShoppingCartQuantity component in the Homepage component:
import React, { useState, useEffect, useRef } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import Subcat from "../../components/subcat/Subcat";
import CategoryService from "../../services/api/Category";
import SubCategoryService from "../../services/api/SubCategory";
import CategoriesLayout from "../../utils/CategoriesLayout";
import CategoryCard from "../../components/category/CategoryCard";
import { Triangle } from 'react-loader-spinner'
import ScrollIntoView from 'react-scroll-into-view'
import ProductService from "../../services/api/Product";
import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content';
import YouTube from 'react-youtube';
import FavoriteBtn from "../../components/favorite/FavoriteBtn";
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
import "./Homepage.css";
import "../../components/product/ProductModal.css"
import "react-loader-spinner";
import modalStyles from "../../components/product/ProductModal.module.css"
function Homepage() {
const [categories, setCategories] = useState([]);
const [subCats, setSubCats] = useState([]);
const [loader, setLoader] = useState(false);
const ResponsiveGridLayout = WidthProvider(Responsive);
const scrollRef = useRef();
const productModal = withReactContent(Swal);
const opts = {
// height: '390',
// width: '640',
playerVars: {
autoplay: 1,
}
};
useEffect(() => {
CategoryService.get().then((response) => {
setCategories(response);
});
}, []);
function showSubCatsHandler(catId) {
setLoader(true);
setSubCats([]);
SubCategoryService.get(catId).then((response) => {
setSubCats(response.data);
setLoader(false);
scrollRef.current.scrollIntoView({ behavior: "smooth" });
});
}
function showProductPopupHandler(productId) {
ProductService.get(productId).then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
<h3 className={modalStyles.header}>{product.AMP_Title}</h3>
<h4 className={`${modalStyles.price} ${modalStyles.header}`}>{"CHf " + product.AMP_Price}</h4>
<img className={modalStyles.image} src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/products/' + product.AMP_Image} />
{
product.descriptions.map((desc, _) => (
<div key={desc.AMPD_GUID}>
{
desc.AMPD_Title === '1' && <h4 className={modalStyles.header}>{product.AMP_Title}</h4>
}
{
desc.AMPD_Image !== '' && <img src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/descriptions/' + desc.AMPD_Image} className={desc.AMPD_Alignment === 'left' ? modalStyles.descImageLeft : modalStyles.descImageRight} />
}
<p className={modalStyles.description}>{desc.AMPD_Description}</p>
</div>
))
}
<br/>
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity productId={product.AMP_GUID} />
</div>
<br/>
{
product.AMP_VideoId !== '' &&
<YouTube
videoId={product.AMP_VideoId}
opts={opts}
/>
}
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (
<div>
<div className="categories-container">
<ResponsiveGridLayout
className="layout"
layouts={ CategoriesLayout }
breakpoints={ { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 } }
cols={ { lg: 8, md: 8, sm: 6, xs: 4, xxs: 2 } }
isDraggable={ false }
>
{
categories.map((cat, index) => (
<div key={index}>
<CategoryCard
category_id = {cat.AMC_GUID}
image = {cat.AMC_Image}
showSubCatsHandler = {showSubCatsHandler}
/>
</div>
))
}
</ResponsiveGridLayout>
{
loader &&
<Triangle
height="100"
width="100"
color='#bcad70'
ariaLabel='loading'
wrapperClass="loader"
/>
}
<div ref={scrollRef}>
{
Object.keys(subCats).map((keyName, _) => (
<Subcat
key={subCats[keyName].AMSC_GUID}
title={ subCats[keyName].AMSC_Title }
products={ subCats[keyName].products }
showProductPopupHandler = {showProductPopupHandler}
/>
))
}
</div>
</div>
</div>
);
}
export default Homepage;
I am using the component in a SweetAlert popup. I guess it's the SweetAlert component that is not getting access to the context. Does anyone have an idea how to pass the context to the SweetAlert component?
UPDATE 2
The accepted solution works great except for 1 small issue: the ShoppingCartQuantity component was not re-rendering inside the SweetAlert popup and the qty would not change visually.
I updated the component by using the qty as a state.
const ShoppingCartQuantity = ({ qty, productId, updateCart }) => {
const [quantity, setQuantity] = useState(qty);
const updateCartHandler = (productId, amount) => {
updateCart(productId, amount);
setQuantity(Math.max(quantity + amount, 1));
}
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCartHandler(productId, -1)}
>
-
</span>
<span className={styles.qty}>{quantity}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCartHandler(productId, 1)}
>
+
</span>
</span>
</div>
)
}
Issue
It's very likely that the sweet alert component is rendered outside your app, and thus, outside the CartContextProvider provider. I just searched the repo docs if there is a way to specify a root element, but this doesn't seem possible since this sweet alert code isn't React specific.
See this other similar issue regarding accessing a Redux context in the alert.
Solution
It doesn't seem possible ATM to access the context value from within the modal, so IMHO a workaround could be to refactor your ShoppingCartQuantity component into a wrapper container component to access the context and a presentation component to receive the context values and any callbacks.
I suggest also just passing the amount you want to increment/decrement the quantity by to updateCart instead of passing a "+"/"-" string and operator comparison.
Example:
export const withShoppingCartContext = Component => props => {
const { cart, removeItem, updateCart } = useContext(CartContext);
return <Component {...props} {...{ cart, removeItem, updateCart }} />;
}
const ShoppingCartQuantity = ({ cart, productId, updateCart }) => {
const qty = cart.find(item => item.id === productId)?.qty ?? 0;
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCart(productId, -1)}
>
-
</span>
<span className={styles.qty}>{qty}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCart(productId, 1)}
>
+
</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
In places in your app where ShoppingCartQuantity component is used within the CartContextProvider decorate it with the withShoppingCartContext HOC and use normally.
ShoppingCart
import ShoppingCartQuantityBase, {
withShoppingCartContext
} from "../../components/layout/shoppingCart/ShoppingCartQuantity";
const ShoppingCartQuantity = withShoppingCartContext(ShoppingCartQuantityBase);
const ShoppingCart = (props) => {
...
return (
...
<ShoppingCartQuantity productId={....} />
...
);
};
In places where ShoppingCartQuantity component is used outside the context, like in the sweet modal, access the context within the React code and pass in the context values and callbacks.
...
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
...
function Homepage() {
...
const { cart, updateCart } = useContext(CartContext);
const productModal = withReactContent(Swal);
...
function showProductPopupHandler(productId) {
ProductService.get(productId)
.then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
...
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity
productId={product.AMP_GUID}
{...{ cart, updateCart }}
/>
</div>
...
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (...);
}
export default Homepage;
Additional Issues
Your context provider is mutating state when updating quantities. When updating nested state you should still create a shallow copy of the array elements that are being updated.
Example:
const CartContextProvider = (props) => {
...
const updateCart = (productId, amount) => {
// only update if item in cart
if (cart.some(item => item.id === productId)) {
// use functional state update to update from previous state
// cart.map creates shallow copy of previous state
setCart(cart => cart.map(item => item.id === productId
? {
...item, // copy item being updated into new object reference
qty: Math.max(item.qty + amount, 1), // minimum quantity is 1
}
: item
));
}
}
const removeItem = (id) => {
setCart(cart => cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{ cart, updateCart, removeItem }}>
{props.children}
</CartContext.Provider>
);
};
You did't show where you are using the ShoppingCart component or the ShoppingCartQuantity component.
Anyway, when you declare a route, you must pass the component, not the root element. So, this line:
<Route exact path="/" element={<Homepage/>}/>
must be
<Route exact path="/" component={Homepage}/>

How to scroll to top at each page change on Pagination component?

I would like to have an automatic scroll up when I change pages thanks to my Pagination component. It works great but I would like to add this feature. I have no idea how to do this..I tried with a tutorial that uses window but it doesn't worked because I've got no redirect, just a component divided into several pages (EventLists)...
Thanks!! Here is my code :
PAGINATION COMPONENT
import PropTypes from 'prop-types';
// import { LinkContainer } from 'react-router-bootstrap';
import './pagination.scss';
const Pagination = ({ postsPerPage, totalPosts, paginate }) => {
const pageNumbers = [];
// eslint-disable-next-line no-plusplus
for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) {
pageNumbers.push(i);
}
return (
<nav expand="lg" id="pagination-navbar">
<ul className="pagination">
{pageNumbers.map((number) => (
<li key={number} className="page-item">
<a
style={{ cursor: 'pointer' }}
onClick={() => paginate(number)}
className="page-link"
>{number}
</a>
</li>
))}
</ul>
</nav>
);
};
Pagination.propTypes = {
postsPerPage: PropTypes.number.isRequired,
totalPosts: PropTypes.number.isRequired,
paginate: PropTypes.func.isRequired,
};
export default Pagination;
FILE THAT USES Pagination
import { useState } from 'react';
import { useSelector } from 'react-redux';
// import react-Bootstrap's component(s)
import {
Row,
} from 'react-bootstrap';
// import { useLocation } from 'react-router-dom';
import SearchBar from 'src/components/SearchBar';
import Pagination from 'src/components/Pagination';
import EventCard from '../EventCard';
import './eventsList.scss';
const EventsList = () => {
// TODO code to retrieve the id with a useLocation (not found yet)
// we use useLocation to retrieve the state of the route
// in which we have stored genreId or regionId
// if location is defined, take me its state
// if the state is defined take me the region
// console.log(location.state); => returns null
const [currentPage, setCurrentPage] = useState(1);
const [postsPerPage] = useState(9);
const { eventsList } = useSelector((state) => state.events);
// Get current posts
const indexOfLastPost = currentPage * postsPerPage;
const indexofFirstPost = indexOfLastPost - postsPerPage;
const currentEvents = eventsList.slice(indexofFirstPost, indexOfLastPost);
// Change page
const paginate = (pageNumber) => setCurrentPage(pageNumber);
return (
<div>
<SearchBar
// we pass a string to change the title according to the page
// we pass the length of the table to boost the results in the title
results={eventsList.length}
message="results"
// genreId={genreId}
// regionId={regionId}
/>
<Row>
{currentEvents.map((item) => (
<EventCard key={item.id} {...item} />
))}
</Row>
<Pagination
postsPerPage={postsPerPage}
totalPosts={eventsList.length}
paginate={paginate}
/>
</div>
);
};
export default EventsList;
You could make use of React Refs: https://reactjs.org/docs/refs-and-the-dom.html.
You create a ref, attach it to the element you want to scroll to, and scroll to that element when the page changes.
Something like:
const EventsList = () => {
const pageTopRef = useRef(null);
const paginate = (pageNumber) => {
setCurrentPage(pageNumber);
pageTopRef.current.scrollIntoView();
};
return (
<div>
...
<Row ref={pageTopRef}>
{currentEvents.map((item) => (
<EventCard key={item.id} {...item} />
))}
</Row>
...
</div>
);
};
This seem to work for me. MUI v5, React
...
const [page, setPage] = useState(props.props.pagination.page);
...
useEffect(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth',
});
}, [page]);
...
const handleChange = (event, value) => {
setIsLoading(true);
setPage(value);
};
...
<Pagination
...
count={pageCount}
page={page}
onChange={handleChange}
...
/>

Hide Show component and highlight link with react scroll

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.

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} />
}

Resources