How to optimize toggling className between different menu items by React? - javascript

I have header component, where I want to toggle className between all the elements of menu (if one of the elements of menu is active and user is clicking to another element - this element become active and all others no). I have a code like this
import React, { useState } from 'react';
import './header.scss';
export const Header = ({ favoriteCount }) => {
const [activeIndex, setActiveIndex] = useState(0);
function toggleClass(index) {
setActiveIndex(index);
}
return (
<header className="header">
<div className="container header-container">
<ul className="header-menu">
<li>
<a
className={
activeIndex === 0
? 'header-menu__link active'
: 'header-menu__link'
}
onClick={() => {
toggleClass(0);
}}
href="##"
>
Characters
</a>
</li>
<li>
<a
className={
activeIndex === 1
? 'header-menu__link active'
: 'header-menu__link'
}
onClick={() => {
toggleClass(0);
}}
href="##"
>
Favorites
</a>
</li>
</ul>
<div className="header-favorite-count">
<i className="far fa-heart"></i>
{favoriteCount}
</div>
</div>
</header>
);
};
and styles to visualise toggling classes
&-menu__link {
color: lightgray;
}
.active {
color: #fff;
}
This approach is working but looks creepy. Maybe somebody knows how to optimize it?

I wouldn't use the index, I'd use the text of the item. I'd also include that text in the href so that there's an indication of what the anchor leads to. To avoid repeated code, you might put the menu items in a reusable array, something like this:
const menuItems = [
"Characters",
"Favorites",
];
export const Header = ({ favoriteCount }) => {
const [activeItem, setActiveItem] = useState("");
const setActiveItem = useCallback((event) => {
setActiveItem(event.currentTarget.href.substring(2));
}, []);
const list = menuItems.map(item =>
<li key={item}>
<a
className={`header-menu__link ${item === activeItem ? "active" : ""}`}
onClick={setActiveItem}
href={"##" + item}>
{item}
</a>
</li>
);
return (
<header className="header">
<div className="container header-container">
<ul className="header-menu">
{list}}
</ul>
<div className="header-favorite-count">
<i className="far fa-heart"></i>
{favoriteCount}
</div>
</div>
</header>
);
};

Related

how to target just one dropdown menu instead of all?

I have a react project where I have 3 dropdown menus side by side and upon clicking one they all will toggle instead of just one. I tried to use a dropdown component but I'm not sure I can get it working correctly with my code. Can you show me how to fix this? I have my code uploaded to code sandbox here link to code due note that it will not display on mobile screens yet so you will need to look at it in the full desktop version of the site.
import { useState } from 'react';
import {
iconHamburger,
iconClose,
iconArrowDark,
iconArrowLight,
logo,
} from '../assets';
import { navLinks } from '../constants';
const Navbar = () => {
const [toggle, setToggle] = useState(false);
return (
<nav className='w-full flex py-6 ml-10 justify-between items-center navbar'>
<img src={logo} alt='blogr' className='w-[75px] h-[30px]' />
<ul className='list-none sm:flex hidden ml-10 justify-start items-center flex-1'>
{navLinks.map((nav, index) => (
<li
key={nav.id}
className={`font-overpass
font-normal
text-[12px] ${index === navLinks.length - 1 ? 'mr-0' : 'mr-10'}
text-white`}>
<a
className='float-left'
onClick={() => setToggle((prev) => !prev)}
href={`#${nav.id}`}>
{nav.title}
<img
className='ml-2 mt-1 cursor-pointer float-right w-[9px] h-[6px]'
src={iconArrowLight}
/>
</a>
<div className={`${toggle ? 'hidden' : 'relative'} mr-10`}>
<ul className='list-none mt-10 absolute'>
{nav.links.map((link, index) => (
<li
key={link.name}
className={`font-overpass text-black cursor-pointer ${
index !== nav.links.length - 1 ? 'mb-4' : 'mb-0'}`}>
{link.name}
</li>
))}
</ul>
</div>
</li>
))}
</ul>
</nav>
);
};
export default Navbar;
navlinks
import { iconArrowLight } from "../assets"
export const navLinks = [
{
id: 'product',
title: 'Product',
img: iconArrowLight,
links: [
{
name: 'Overview'
},
{
name: 'Pricing'
},
{
name: 'Marketplace'
},
{
name: 'Features'
},
{
name: 'Integrations'
},
],
},
{
id: 'company',
title: 'Company',
img: iconArrowLight,
links: [
{
name: 'About'
},
{
name: 'Team'
},
{
name: 'Blog'
},
{
name: 'Career'
},
],
},
{
id: 'connect',
title: 'Connect',
img: iconArrowLight,
links: [
{
name: 'Contact'
},
{
name: 'Newsletter'
},
{
name: 'LinkedIn'
},
],
},
]
All the drop list is sharing one toggle state, therefore they open and close at same time.
You could make each drop list a separate component DropList, each will have an individual state of toggle.
It will allow adding more DropList easier, also keeping the code in Navbar cleaner.
Full example: (live modified project: codesandbox)
import { useState } from "react";
import {
iconHamburger,
iconClose,
iconArrowDark,
iconArrowLight,
logo,
} from "../assets";
import { navLinks } from "../constants";
const Navbar = () => {
return (
<nav className="w-full flex py-6 ml-10 justify-between items-center navbar">
<img src={logo} alt="blogr" className="w-[75px] h-[30px]" />
<ul className="list-none sm:flex hidden ml-10 justify-start items-center flex-1">
{navLinks.map((nav, index, arr) => (
<DropList
key={nav.id}
mr={index === arr.length - 1 ? "mr-0" : "mr-10"}
{...nav}
/>
))}
</ul>
</nav>
);
};
export default Navbar;
export const DropList = ({ id, title, links, mr }) => {
const [toggle, setToggle] = useState(false);
return (
<li
className={`font-overpass
font-normal
text-[12px] ${mr}
text-white`}
>
<a
className="float-left"
onClick={() => setToggle((prev) => !prev)}
href={`#${id}`}
>
{title}
<img
className="ml-2 mt-1 cursor-pointer float-right w-[9px] h-[6px]"
src={iconArrowLight}
/>
</a>
<div className={`${toggle ? "hidden" : "relative"} mr-10`}>
<ul className="list-none mt-10 absolute">
{links.map((link, index, arr) => (
<li
key={link.name}
className={`font-overpass text-black ${
index !== arr.length - 1 ? "mb-4" : "mb-0"
}`}
>
{link.name}
</li>
))}
</ul>
</div>
</li>
);
};
You use the same state to toggle all drop downs. Normally I would recommend to create a new component for the drop down to keep the state. However since you specified that you only want one open at any time, I would recommend to change the state to keep the id of the open nav item.
import { useState } from "react";
import { iconArrowLight, logo } from "../assets";
import { navLinks } from "../constants";
const Navbar = () => {
const [openId, setOpenId] = useState(undefined);
return (
<nav className="w-full flex py-6 ml-10 justify-between items-center navbar">
<img src={logo} alt="blogr" className="w-[75px] h-[30px]" />
<ul className="list-none sm:flex hidden ml-10 justify-start items-center flex-1">
{navLinks.map((nav, index) => (
<li
key={nav.id}
className={`font-overpass
font-normal
text-[12px] ${index === navLinks.length - 1 ? "mr-0" : "mr-10"}
text-white`}
>
<a
className="float-left"
onClick={() =>
setOpenId((prev) => (prev === nav.id ? undefined : nav.id)) // If the id is the same as the previous id, close it
}
href={`#${nav.id}`}
>
{nav.title}
<img
className="ml-2 mt-1 cursor-pointer float-right w-[9px] h-[6px]"
src={iconArrowLight}
/>
</a>
<div
className={`${openId !== nav.id ? "hidden" : "relative"} mr-10`}
>
<ul className="list-none mt-10 absolute">
{nav.links.map((link, index) => (
<li
key={link.name}
className={`font-overpass text-black cursor-pointer ${
index !== nav.links.length - 1 ? "mb-4" : "mb-0"
}`}
>
{link.name}
</li>
))}
</ul>
</div>
</li>
))}
</ul>
</nav>
);
};
export default Navbar;

Window is not defined in nextJS

I am getting the error "window is not defined" in nextJS project. Here isMobile is storing the value that window size is less than 767.98 or not to execute the open/close hamburger menu functionality. This code was working fine in ReactJS but not working in NextJS. Please help me to figure out this issue.
import Link from 'next/link';
import React, { useState, useEffect, useRef } from "react";
const Navbar = () => {
const isMobile = window.innerWidth <= 767.98;
const [isMenuOpen, setIsMenuOpen] = useState(!isMobile);
const toggle = () => isMobile && setIsMenuOpen(!isMenuOpen);
const ref = useRef()
useEffect(() => {
if (isMobile) {
const checkIfClickedOutside = (e) => {
if (!ref.current?.contains(e.target)) {
setIsMenuOpen(false);
}
};
document.addEventListener("mousedown", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside);
};
}
}, []);
return (
<>
<header>
<nav>
<div className="nav">
<div className="nav-brand">
<Link href="/" className="text-black"><a>Website</a></Link>
</div>
<div ref={ref}>
<div className="toggle-icon" onClick={toggle}>
<i id="toggle-button" className={isMenuOpen ? 'fas fa-times' : 'fas fa-bars'} />
</div>
{isMenuOpen && (
<div className={isMenuOpen ? "nav-menu visible" : "nav-menu"}>
<ul className="main-menu">
<li><Link href="/" onClick={toggle}><a>Home</a></Link></li>
<li><Link href="/blog" onClick={toggle}><a>Blog</a></Link></li>
<li className="drp">
<p className="dropbtn">Find <i className="fa-solid fa-angle-down"></i></p>
<ul className="dropdown-content">
<li><Link href="/find/portable-keyboards" onClick={toggle}><a>Portable Keyboards</a></Link></li>
</ul>
</li>
</ul>
</div>
)}
</div>
</div>
</nav>
</header>
</>
)
}
export default Navbar;
Next.js is a server-side rendering framework which means the initial call to generate HTML from the server. At this point, window object, is only available on the client-side (not on the server-side).
To solve this problem, you need to check window object availability.
import Link from 'next/link';
import React, { useState, useEffect, useRef } from "react";
const Navbar = () => {
const isMobile = typeof window !== "undefined" && window.innerWidth <= 767.98
const [isMenuOpen, setIsMenuOpen] = useState(!isMobile);
const toggle = () => isMobile && setIsMenuOpen(!isMenuOpen);
const ref = useRef()
useEffect(() => {
if (isMobile) {
const checkIfClickedOutside = (e) => {
if (!ref.current?.contains(e.target)) {
setIsMenuOpen(false);
}
};
document.addEventListener("mousedown", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside);
};
}
}, []);
return (
<>
<header>
<nav>
<div className="nav">
<div className="nav-brand">
<Link href="/" className="text-black"><a>Website</a></Link>
</div>
<div ref={ref}>
<div className="toggle-icon" onClick={toggle}>
<i id="toggle-button" className={isMenuOpen ? 'fas fa-times' : 'fas fa-bars'} />
</div>
{isMenuOpen && (
<div className={isMenuOpen ? "nav-menu visible" : "nav-menu"}>
<ul className="main-menu">
<li><Link href="/" onClick={toggle}><a>Home</a></Link></li>
<li><Link href="/blog" onClick={toggle}><a>Blog</a></Link></li>
<li className="drp">
<p className="dropbtn">Find <i className="fa-solid fa-angle-down"></i></p>
<ul className="dropdown-content">
<li><Link href="/find/portable-keyboards" onClick={toggle}><a>Portable Keyboards</a></Link></li>
</ul>
</li>
</ul>
</div>
)}
</div>
</div>
</nav>
</header>
</>
)
}
export default Navbar;
Another way you can fix it is you can move that window logic into useEffect (or componentDidMount on a class-based component)
import Link from 'next/link';
import React, { useState, useEffect, useRef } from "react";
const Navbar = () => {
const [isMobile, setIsMobile] = useState(false); //the initial state depends on mobile-first or desktop-first strategy
const [isMenuOpen, setIsMenuOpen] = useState(true);
const toggle = () => isMobile && setIsMenuOpen(!isMenuOpen);
const ref = useRef()
useEffect(() => {
setIsMobile(window.innerWidth <= 767.98)
setIsMenuOpen(window.innerWidth > 767.98)
}, [])
useEffect(() => {
if (isMobile) {
const checkIfClickedOutside = (e) => {
if (!ref.current?.contains(e.target)) {
setIsMenuOpen(false);
}
};
document.addEventListener("mousedown", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside);
};
}
}, [isMobile]);
return (
<>
<header>
<nav>
<div className="nav">
<div className="nav-brand">
<Link href="/" className="text-black"><a>Website</a></Link>
</div>
<div ref={ref}>
<div className="toggle-icon" onClick={toggle}>
<i id="toggle-button" className={isMenuOpen ? 'fas fa-times' : 'fas fa-bars'} />
</div>
{isMenuOpen && (
<div className={isMenuOpen ? "nav-menu visible" : "nav-menu"}>
<ul className="main-menu">
<li><Link href="/" onClick={toggle}><a>Home</a></Link></li>
<li><Link href="/blog" onClick={toggle}><a>Blog</a></Link></li>
<li className="drp">
<p className="dropbtn">Find <i className="fa-solid fa-angle-down"></i></p>
<ul className="dropdown-content">
<li><Link href="/find/portable-keyboards" onClick={toggle}><a>Portable Keyboards</a></Link></li>
</ul>
</li>
</ul>
</div>
)}
</div>
</div>
</nav>
</header>
</>
)
}
export default Navbar;
Note that, with this solution, your UI may have some flickering due to isMobile state
You can try this on your parent component definition.
import dynamic from 'next/dynamic'
const Navbar = dynamic(() => import('./Navbar'), { ssr: false });
const Parent = () => {
...
return (
{(typeof window !== 'undefined') &&
<Navbar/>
}
...
<Footer/>
);
}

Hide other previous div/dropdown React

I am wanting to hide other sibling divs (dropdowns in my case) when I click the statusPillDropdown
so far I click I am setting the status to true and opening the div,
{DropDown ${toggleStatusDropdown ? "open": ""}}
Do I just need to set the state to false for previously opened ones? Not sure how to do this.
thank you
import React, { useState } from "react";
import "./StatusPillDropdown.scss";
function StatusPillDropdown({
cellData,
rowItemId,
onStatusPillDropdownChange
}) {
const [toggleStatusDropdown, setToggleStatusDropdown] = useState();
const toggleDropdown = (action, rowItemId, e) => {
if (action === "pillClick") {
setToggleStatusDropdown(true);
} else {
onStatusPillDropdownChange(rowItemId, e.target.getAttribute("value"));
setToggleStatusDropdown(false);
}
};
const renderstatusPillDropdown = (cellData, rowItemId) => (
<React.Fragment>
<span
className="statusPillDropdown"
onClick={() => toggleDropdown("pillClick", rowItemId)}
>
<span className={`status-pill ${cellData.type}`}>{cellData.text}</span>
</span>
<div className="status">
<div className="dropdown-container">
<div className={`DropDown ${toggleStatusDropdown ? "open" : ""}`}>
<ul>
<li
value="Information only"
onClick={e => toggleDropdown("liClick", rowItemId, e)}
>
<span></span>Information only
</li>
<li
value="Unresolved"
onClick={e => toggleDropdown("liClick", rowItemId, e)}
>
<span className="unresolved"></span>Unresolved
</li>
<li
value="Partially Resolved"
onClick={e => toggleDropdown("liClick", rowItemId, e)}
>
<span className="partyResolved"></span>Partially Resolved
</li>
<li
value="Fully Resolved"
onClick={e => toggleDropdown("liClick", rowItemId, e)}
>
<span className="resolved"></span>Fully Resolved
</li>
</ul>
</div>
</div>
</div>
</React.Fragment>
);
return (
<React.Fragment>
{renderstatusPillDropdown(cellData, rowItemId)}
</React.Fragment>
);
}
export default StatusPillDropdown;

toggle background color on a function - ReactJS

When I clicked a link it renders a view based on the "id" from a JSON. I need to apply a background color when a certain view renders. And I should toggle the Style.
This code shows the crawl when I clicked a particular link.
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { [id]: !current.showCrawl[id] }
}));
};
This is my render method where Ia am mapping the links and the additional details on JSON
render() {
return (
<div class="d-flex" id="wrapper">
<div class="bg-light border-right" id="sidebar-wrapper">
<h1 class="sidebar-heading">API</h1>
<ul class="list-group list-group-flush">
{this.state.apis.map(api => (
<li><a class="list-group-item list-group-item-action bg-light" key={api.id}
id={api.id}
onClick={this.handleCrawl}>{api.title}</a></li>
))}
</ul>
</div>
<div id="page-content-wrapper">
<div class="container-fluid">
{this.state.apis.map(api => (
<div
key={api.id}
id={api.id}>
{this.state.showCrawl[api.id] && (
<SwaggerUI url={api.opening_crawl}/>
)}
</div>
))}
</div>
</div>
</div>
);
}
Not sure if it answers your question.
You can toggle the style conditionally.
{this.state.apis.map(api => (
<div
key={api.id}
id={api.id}
className={this.state.showCrawl[api.id]?"some-specific-style":"default-or-empty"}
>
{this.state.showCrawl[api.id] && (
<SwaggerUI url={api.opening_crawl}/>
)}
</div>
))}

React Collapse - how do I toggle items in the list?

I am displaying a list of items from database and for each item, I have a button "Show more/less". When this is clicked, I want to show/hide the extra content with a nice slide down/up effect. I have implemented this functionality without the slide down/up effect, but want to use React Collapse to make it more user-friendly.
Here's the component where I am trying to implement the React Collapse functionality:
import React from 'react';
import ReactDOM from 'react-dom';
//import axios from 'axios';
import NewPost from './NewPost';
import {Collapse} from 'react-collapse';
class Posts extends React.Component {
constructor(props) {
super(props);
this.toggleClass= this.toggleClass.bind(this);
this.state = {
activeIndex: null
}
}
toggleClass(index, e) {
this.setState({ activeIndex: this.state.activeIndex === index ? null : index });
};
moreLess(index) {
if (this.state.activeIndex === index) {
return (
<span>
<i className='fas fa-angle-up'></i> Less
</span>
);
} else {
return (
<span>
<i className='fas fa-angle-down'></i> More
</span>
);
}
}
render () {
let content;
if (this.props.loading) {
content = 'Loading...';
} else {
content = this.props.posts.map((post, key) => {
return(
<li key={key}>
<div>
<span>{post.id}</span>
<span>{post.message}</span>
<button className="btn btn-primary btn-xs" onClick={this.toggleClass.bind(this, key)}>
{this.moreLess(key)}
</button>
</div>
<Collapse isOpened={true}>
<div className={'alert alert-info msg '+(this.state.activeIndex === key ? "show" : "hide")}>
{post.message}
</div>
</Collapse>
</li>
)
});
}
return (
<div>
<h1>Posts!</h1>
<div className="row">
<div className="col-md-6">
<ul>
{content}
</ul>
</div>
</div>
</div>
);
}
}
export default Posts
But when I click on the More/less button, the content in Collapse doesn't appear - after clicking the button nothing happens.
What am I missing here yet?
if you're using function and hooks I recommend this
import { Collapse } from "react-collapse";
import classNames from "classnames";
import React, { useState} from 'react';
export default function yourFunction() {
const [activeIndex, setActiveIndex] = useState(null);
return(
{groups.map((group, index) => (
<button className="btn btn-primary navbar-toggler"
type="button"
data-toggle="collapse"
onClick={event => setActiveIndex(
activeIndex === index ? null : index
)}
data-target="#collapseExample"
aria-expanded="false"
aria-controls="collapseExample">
[CLICK HERE]
</button>
<Collapse isOpened={activeIndex === index}>
<div
className={classNames("alert alert-info msg", {
show: activeIndex === index,
hide: activeIndex !== index
})}
>
<a>[YOUR COLLAPSE CONTENT]</a>
</div>
</Collapse>
)
}
You didn't bind correct check to <Collapse isOpened={true}>. Instead of true, you should put (this.state.)activeIndex === index (current item index) like this:
<Collapse isOpened={this.state.activeIndex === index}>
So it can actually collapse due to activeIndex. I've made codesandbox for you so you can make sure it works: https://codesandbox.io/s/jzx44ynyqw
But I think this is the most important part of it (note that your index was called key, I just renamed it for convenience):
<li key={index}>
<div>
<p>{post.title}</p>
<Collapse isOpened={activeIndex === index}>
<div
className={classNames("alert alert-info msg", {
show: activeIndex === index,
hide: activeIndex !== index
})}
>
{post.message}
</div>
</Collapse>
<button
className="btn btn-primary btn-xs"
onClick={this.toggleClass.bind(this, index)}
>
{this.moreLess(index)}
</button>
</div>
</li>

Categories

Resources