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}
...
/>
Related
I have an image slider component and a simple custom hook that gets the refElement and the width of the element using the useRef hook. -
The code sandbox is here Image Slider
When I use the slider component and just map the data in without filtering, everything works fine. If I filter and map the data then I get Uncaught TypeError: elementRef.current is undefined . (In the sandbox you have to comment out the second instance (unfiltered) of SliderTwo to recreate the error. Why does it work without the filter but not with (when rendered by itself)? More in depth explanation below.
useSizeElement()
import { useState, useRef, useEffect } from 'react';
const useSizeElement = () => {
const [width, setWidth] = useState(0);
const elementRef = useRef();
useEffect(() => {
setWidth(elementRef.current.clientWidth); // This will give us the width of the element
}, [elementRef.current]);
return { width, elementRef };
};
export default useSizeElement;
I call the hook (useSizeElement) inside of a context because I need the width to use in another hook in a different component thus:
context
import React, { createContext, useState, useEffect} from 'react';
import useSizeElement from '../components/flix-slider/useSizeElement';
export const SliderContext = createContext();
export const SliderProvider = ({children}) => {
const { width, elementRef } = useSizeElement();
const [currentSlide, setCurrentSlide] = useState();
const [isOpen, setIsOpen] = useState(false)
console.log('context - width', width, 'elementRef', elementRef)
const showDetailsHandler = movie => {
setCurrentSlide(movie);
setIsOpen(true)
};
const closeDetailsHandler = () => {
setCurrentSlide(null);
setIsOpen(false)
};
const value = {
onShowDetails: showDetailsHandler,
onHideDetails: closeDetailsHandler,
elementRef,
currentSlide,
width,
isOpen
};
return <SliderContext.Provider value={value}>{children}</SliderContext.Provider>
}
I get the width of the component from the elementRef that was passed from the context.-
Item Component
import React, { Fragment, useContext } from 'react';
import { SliderContext } from '../../store/SliderContext.context';
import ShowDetailsButton from './ShowDetailsButton';
import Mark from './Mark';
import { ItemContainer } from './item.styles';
const Item = ({ show }) => {
const { onShowDetails, currentSlide, isOpen, elementRef } =
useContext(SliderContext);
const isActive = currentSlide && currentSlide.id === show.id;
return (
<Fragment>
<ItemContainer
className={isOpen ? 'open' : null}
ref={elementRef}
isActive={isActive}
isOpen={isOpen}
>
<img
src={show.thumbnail.regular.medium}
alt={`Show title: ${show.title}`}
/>
<ShowDetailsButton onClick={() => onShowDetails(show)} />
</ItemContainer>
</Fragment>
);
};
export default Item;
The width is passed using context where another hook is called in the Slider Component:
Slide Component
import useSizeElement from './useSizeElement';
import { OuterContainer } from './SliderTwo.styles';
const SliderTwo = ({ children }) => {
const {currentSlide, onHideDetails, isOpen, width, elementRef } = useContext(SliderContext);
const { handlePrev, handleNext, slideProps, containerRef, hasNext, hasPrev } =
useSliding( width, React.Children.count(children));
return (
<Fragment>
<SliderWrapper>
<OuterContainer isOpen={isOpen}>
<div ref={containerRef} {...slideProps}>
{children}
</div>
</OuterContainer>
{hasPrev && <SlideButton showLeft={hasPrev} onClick={handlePrev} type="prev" />}
{hasNext && <SlideButton showRight={hasNext} onClick={handleNext} type="next" />}
</SliderWrapper>
{currentSlide && <Content show={currentSlide} onClose={onHideDetails} />}
</Fragment>
);
};
export default SliderTwo;
Now everything works fine if I just map the data with no filters into the slider as shown in the sandbox. But if I apply a filter to display only what I want I get -
Uncaught TypeError: elementRef.current is undefined
I do know that you can't create a ref on an element that does not yet exist and I've seen examples where you can use useEffect to get around it but I can't find the solution to get it to work.
Here is the App.js - To see the error I'm getting, comment out the second instance of . As long as I'm running one instance without filtering the data, it works, but it won't work by itself.
import { useState, useEffect, Fragment } from "react";
import SliderTwo from "./components/SliderTwo";
import Item from "./components/Item";
import shows from "./data.json";
import "./App.css";
function App() {
const [data, setData] = useState(null);
const datafunc = () => {
let filteredData = shows.filter((show) => {
if (show.isTrending === true) {
return show;
}
});
setData(filteredData);
};
useEffect(() => {
datafunc();
}, []);
console.log("Trending movies", data);
return (
<Fragment>
<div className="testDiv">
{shows && data && (
<SliderTwo>
{data && data.map((show) => <Item show={show} key={show.id} />)}
</SliderTwo>
)}
</div>
<div className="testDiv">
<SliderTwo>
{shows.map((show) => (
<Item show={show} key={show.id} />
))}
</SliderTwo>
</div>
</Fragment>
);
}
export default App;
Full code: Sandbox - https://codesandbox.io/s/twilight-sound-xqglgk
I think it may be an issue when the useSizeElement is first mounted as the useEffect will run once at the beginning of each render.
When it runs at the first instance and the ref is not yet defined so it was returning the error: Cannot read properties of undefined (reading 'clientWidth')
If you modify your code to this I believe it should work:
import { useState, useRef, useEffect } from "react";
const useSizeElement = () => {
const [width, setWidth] = useState(0);
const elementRef = useRef();
useEffect(() => {
if (elementRef.current) setWidth(elementRef.current.clientWidth); //
This will give us the width of the element
}, [elementRef]);
return { width, elementRef };
};
export default useSizeElement;
This way you are checking if the elementRef is defined first before setting the width
UPDATE:
<Fragment>
<div className="testDiv">
<SliderTwo>
{shows
.filter((show) => {
if (show.isTrending === true) {
return show;
}
return false;
})
.map((show) => (
<Item show={show} key={show.id} />
))}
</SliderTwo>
</div>
{/* <div className="testDiv">
<SliderTwo>
{shows.map((show) => (
<Item show={show} key={show.id} />
))}
</SliderTwo>
</div> */}
</Fragment>
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}/>
I have filtered the products and on submitting the search term, am showing the results in a new page using history.push() property.
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { IoIosSearch } from 'react-icons/io';
import { useHistory } from "react-router-dom";
import './style.css';
/**
* #author
* #function Search
*/
const Search = (props) => {
const product = useSelector(state => state.product);
let { products , filteredProducts } = product;
const [searchTerm, setSearchTerm] = useState('');
const onChangeSearch = (e) => {
setSearchTerm(e.currentTarget.value);
}
const isEmpty = searchTerm.match(/^\s*$/);
if(!isEmpty) {
filteredProducts = products.filter( function(prod) {
return prod.name.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase().trim())
})
}
const history = useHistory();
const display = !isEmpty
const handleSubmit =(e) => {
e.preventDefault();
if( !isEmpty ) {
history.push(`/search/search_term=${searchTerm}/`, { filteredProducts })
}
setSearchTerm('');
}
return (
<div>
<form onSubmit={handleSubmit}>
<div className="searchInputContainer">
<input
className="searchInput"
placeholder={'What are you looking for...'}
value={searchTerm}
onChange={onChangeSearch}
/>
<div className="searchIconContainer">
<IoIosSearch
style={{
color: 'black',
fontSize: '22px'
}}
onClick={handleSubmit}
/>
</div>
</div>
</form>
{
display && <div className="searchResultsCont">
{filteredProducts.map((prod, index) => (<div key={index}>{prod.name}</div>))}
</div>
}
</div>
);
}
export default Search
On the new page this is the code :
import React from 'react';
import Layout from '../../components/Layout';
const SearchScreen = ({location}) => {
const products = location.state.filteredProducts;
const show = products.length > 0
return (
<Layout>
<div>
{
show ? products.map((prod, index) => (<div key={index}>{prod.name}</div>)) : <div>No items found</div>
}
</div>
</Layout>
)
}
export default SearchScreen
The problem comes when I copy and paste the URL to another new page, or like when I email others the URL the error becomes " Cannot read property 'filteredProducts' of undefined ". Using this method I understand that the results (filtered products) have not been pushed through the function history.push() that's why it is undefined, how can I make this possible?
I changed the whole aspect to filtering the products from the back-end..
It worked
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} />
}
The parent component connects to a Google Cloud FireStore and saves all data in to cards using setCards hooks.
Next we import two children components in to our parent component:
<UpdateCard card={card} />
<AddCard totalDoclNumbers={totalDoclNumbers} />
PARENT Component - DockList
import React, { useState, useEffect } from 'react';
import { db } from '../firebase';
import UpdateCard from './UpdateCard';
import AddCard from './AddCard';
const DocList = () => {
const [cards, setCards] = useState([]);
const [beginAfter, setBeginAfter] = useState(0);
const [totalDoclNumbers, setTotalDoclNumbers] = useState(0);
useEffect(() => {
const fetchData = async () => {
const data = await db
.collection('FlashCards')
.orderBy('customId')
.startAfter(beginAfter)
.get();
setCards(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
};
fetchData();
}, [beginAfter]);
return (
<ul className='list'>
{cards.map((card) => (
<li key={card.id} className='list__item' data-id={card.id}>
<UpdateCard card={card} />
</li>
))}
<AddCard totalDoclNumbers={totalDoclNumbers} />
</ul>
);
};
export default DocList;
Inside UpdateCard, we list all data stored in cards using an unordered list:
import React, { useState } from 'react';
import { db } from '../firebase';
const UpdateCard = ({ card }) => {
const [translatedText, setTranslatedText] = useState(card.translatedText);
const [customId, setCustomId] = useState(card.customId);
const onUpdate = async () => {
await db
.collection('FlashCards')
.doc(card.id)
.update({ ...card, customId, originalText, translatedText, imgURL });
};
return (
<>
<input
type='text'
value={customId}
onChange={(e) => {
setCustomId(Number(e.target.value));
}}
/>
<textarea
value={translatedText}
onChange={(e) => {
setTranslatedText(e.target.value);
}}
/>
<button onClick={onUpdate}>
Update
</button>
</>
);
};
export default UpdateCard;
Finally in the second child component, called AddCard, we have a button, which triggers the function onAdd to add new data in to our FireStore collection.
import React, { useState } from 'react';
import { db } from '../firebase';
const AddCard = ({ totalDoclNumbers }) => {
const [newTranslatedText, setNewTranslatedText] = useState([]);
const nextNumber = totalDoclNumbers + 1;
const onAdd = async () => {
await db.collection('FlashCards').add({
translatedText: newTranslatedText,
customId: Number(nextNumber),
});
};
return (
<ul className='list'>
<li key={nextNumber}>
<input
type='text'
className='list__input'
defaultValue={nextNumber}
/>
<textarea
onChange={(e) => setNewTranslatedText(e.target.value)}
/>
<button onClick={onAdd}>
Add
</button>
</li>
</ul>
);
};
export default AddCard;
It all works. When you click the button inside the second child component AddCard component, the new data get stored in to the collection.
But to be able to see new added data, I need to render UpdateCard and that's exactly, what I'm struggling with.
How can I achieve that click on the button inside the AddCard component, triggers rendering in UpdateCard component.
Ok, so first on DocList add a callback function:
const DocList = () => {
...
const [addButtonClickCount, setAddButtonClickCount] = useState(0);
...
return (
<ul className='list'>
{cards.map((card) => (
<li key={card.id} className='list__item' data-id={card.id}>
<UpdateCard card={card} addButtonClickCount={addButtonClickCount}/>
</li>
))}
<AddCard totalDoclNumbers={totalDoclNumbers} onAddButtonClick={(card) => {
setAddButtonClickCount(c => c + 1)
setCards(cards => [...cards, {...card.data(), id: card.idcard}])
}} />
</ul>
);
};
then call onAddButtonClick which is passed to AddCard as props when needed:
const AddCard = ({ totalDoclNumbers, onAddButtonClick }) => {
...
const onAdd = async () => {
// Somehow you gotta get value of newly created card:
let card = await db.collection('FlashCards').add({
translatedText: newTranslatedText,
customId: Number(nextNumber),
});
// pass the newly created card here so you could use it in `UpdateCard`
onAddButtonClick(card) // this looks likes where it belongs.
};
this will result in rerendering of UpdateCard component since it's getting addButtonClickCount as props, if you want to do something in UpdateCard after add button is clicked, you could use useEffect with [addButtonClickCount] dependency array.