I need help with my code. The thing I want create is to change className according to page url
So when I scroll or go to page /kontakt I want to change class from "hamburger" to "hamburger active".
I also tried regex. Any ideas?
Here is code:
const HamMenu = ()=> {
const [sidebar, setSidebar] = useState(false)
const [burger, setBurger] = useState(false)
const url = window.location.href;
const showSidebar = () => setSidebar(!sidebar)
const changeColor = () => {
if((window.scrollY >= 60) || (url.indexOf("kontakt") > -1)){
setBurger(true);
} else {
setBurger(false);
}
}
window.addEventListener('scroll', changeColor);
return (
<StyledMenu>
<div>
<Link to="#" className={sidebar ? 'menu-bars open' : 'menu-bars'} >
<FontAwesomeIcon
icon={faBars}
size="2x"
className={burger ? 'hamburger active' : 'hamburger'}
onClick={showSidebar}
/>
</Link>
</div>
Dealing with window in Gatsby could be a little bit tricky because two fundamental reasons:
window object is only defined in the browser, so it will work perfectly under gatsby develop but you will need to add a "hack" to avoid a code-breaking in the gatsby build (because there's no window in the Node server).
Treating the window outside React ecosystem, may break the rehydration of the components. This means that React won't potentially know what components need to re-render on-demand, causing unmounted components, especially when navigating forward and backward using the browser's history.
There are a few workarounds to achieve what you're trying to do.
Gatsby, by default, provides a location prop in all top-level components (pages). So you can pass it to any child components at any time to change the class name based on its value:
const IndexPage = ({ location }) =>{
return <Layout>
<HamMenu location={location} />
<h1> some content</h1>
</Layout>
}
Then, in your <HamMenu> component:
const HamMenu = ({ location })=> {
const [sidebar, setSidebar] = useState(false)
const [burger, setBurger] = useState(false)
const url = window.location.href;
const showSidebar = () => setSidebar(!sidebar)
const changeColor = () => {
if((window.scrollY >= 60) || (url.indexOf("kontakt") > -1)){
setBurger(true);
} else {
setBurger(false);
}
}
useEffect(() => {
if(typeof window !== "undefined"){
const url = window.location.href
const changeColor = () => {
setBurger(window.scrollY >= 60 || url.contains("kontakt"))
}
window.addEventListener('scroll', changeColor)
return () => {
window.removeEventListener('scroll', changeColor)
}
}
}, [])
return (
<StyledMenu>
<div>
<Link to="#" className={sidebar ? 'menu-bars open' : 'menu-bars' location.pathname.includes("your-page")? ''some-class' : 'some-other-class' } >
<FontAwesomeIcon
icon={faBars}
size="2x"
className={burger ? 'hamburger active' : 'hamburger'}
onClick={showSidebar}
/>
</Link>
</div>
I would suggest another approach to get the scroll position rather than using directly the window, using React-based approach to avoid what I was pointing before (How to add a scroll event to a header in Gatsby).
However, I've fixed your initial approach, wrapping it inside a useEffect with empty deps ([]). This function will be triggered once the DOM tree is loaded, to avoid the code-breaking window use that I was talking about. Alternatively to url.indexOf("kontakt") > -1 you may want to use url.includes("kontakt") which is way more readable.
Regarding the rest, it's quite self-explanatory. Destructuring the location props you get access to a bunch of data, the pathname property holds the page name so based on that, you can add a ternary condition wherever you want, such as location.pathname.includes("your-page") ? ''some-class' : 'some-other-class' (includes is more semantic in my opinion).
As you see, I've fixed your approach but I've also added a React/Gatsby-based one, choose what makes you feel comfortable.
React components rendering server-side (such as during gatsby build) do not have access to window, and in order to avoid breaking hydration, the first render needs to match what is rendered server-side. For these reasons, you'll want to use useEffect to make client-side changes that rely on window after the component mounts.
Note that this solution is going to perform rather poorly since changeColor is calling setBurger on each scroll event, which prompts the component to be re-rendered (even if the value is the same). You'll want to add a debounce or throttle routine to mitigate this.
const HamMenu = ()=> {
const [burger, setBurger] = useState(false)
useEffect(() => {
const url = window.location.href
const changeColor = () => {
setBurger(window.scrollY >= 60 || url.contains("kontakt"))
}
window.addEventListener('scroll', changeColor)
return () => {
window.removeEventListener('scroll', changeColor)
}
}, [])
return (
<StyledMenu>
<div>
<Link to="#" className={sidebar ? 'menu-bars open' : 'menu-bars'} >
<FontAwesomeIcon
icon={faBars}
size="2x"
className={burger ? 'hamburger active' : 'hamburger'}
/>
</Link>
</div>
</StyledMenu>
)
}
Related
I once read that accessing the dom directly is considered bad practice when using react JS and wanted to clarify a use case in an accordion component I built. The component needs to animate when it expands/collapses so I decided to use CSS transitions for this.
To achieve this I essentially stick the element with the content of the accordion tab into an array which which is stored in a useRef object. Then, when the user clicks the trigger, I access the ref and alter the style property of the element, and other related element, by using methods such as firstElementChild and closest.
The resulting component is pretty clean and the method works well, I was just concerned whether or not there is something I am missing. I have seen multiple articles online which either use a library or nested hooks which ultimately use setTimeout to apply a style. This feels hacky to me.
Anyway, some code...
Event handlers:
const tabsRef = useRef<TabRef[]>([]);
const indRef = useRef<number[]>([]);
const alterTabs = (newIndexes: number[]) => {
tabsRef.current.forEach((tab) => {
tab.element.style.setProperty(
"height",
newIndexes.includes(tab.index)
? `${tab?.element?.firstElementChild?.clientHeight}px`
: "0px"
);
tab.element
?.closest("[aria-expanded]")
?.setAttribute("aria-expanded", `${newIndexes.includes(tab.index)}`);
});
};
const selectDay: React.MouseEventHandler = (
event: React.MouseEvent<HTMLButtonElement>
) => {
const trigger = event.currentTarget;
trigger?.setAttribute("disabled", "true");
const triggeredIndex = Number(trigger.dataset.index);
const newIndexes = indRef.current.includes(triggeredIndex)
? [...indRef.current.filter((index) => index !== triggeredIndex)]
: [...indRef.current, triggeredIndex];
alterTabs(newIndexes);
indRef.current = newIndexes;
trigger?.removeAttribute("disabled");
};
Relevant Render method:
{items.map((item, i) => (
<AccordionTriggerWrapper
key={Math.random().toString(36).substring(2, 9)}
data-index={i}
aria-expanded="false"
>
<AccordionTrigger>
<Button margin="sm" data-index={i} onClick={selectDay}>
<AccordionTitleIcon>
{item.icon && getIcon(item.icon)}
{item.title}
</AccordionTitleIcon>
{[getIcon("minus", 10), getIcon("plus", 10)]}
</Button>
</AccordionTrigger>
<Content>
<ContentInner
ref={(element) => {
tabsRef.current[i] = {
index: i,
element: element?.parentElement as HTMLElement,
};
}}
>
<ReactMarkdown
key={Math.random().toString(36).substring(2, 9)}
>
{item.content}
</ReactMarkdown>
</ContentInner>
</Content>
</AccordionTriggerWrapper>
))}
It's bad practice to dominate the DOM by yourself but small things like you do above, you can do it. Anyway, I can recommend you some react styling components like react-transition-group(especially), and you can take a look at React-Motion and React-Move. hit me up if it's helpful
I am trying to make a hide/show navbar in ReactJS.
But on the first click I am getting this error and its working fine after the first click.
Note: Its twice and i have no idea why
Here is my code and its only working (after first error) when
setMenu(!menu);
is before
nav.classList.toggle("nav-open");
otherwise the error just keeps coming.
export default function Navbar() {
const [menu, setMenu] = useState(true);
const nav = document.querySelector("nav");
const openNav = () => {
setMenu(!menu);
nav.classList.toggle("nav-open");
};
return (
<nav id="nav-wrapper">
<header className="nav-header">
<div
className="arrow-btn"
onClick={() => {
openNav();
}}
>
{menu ? (
<Icon.HiChevronDoubleLeft size={20} className="arrows" />
) : (
<Icon.HiChevronDoubleRight size={20} className="arrows" />
)}
</div>
<img src={Profiledp} alt="." />
<p className="name">Naman Pokhriyal</p>
</header>
</nav>
This isn't the right way of doing this in React in the first place. You're already using the menu state value to determine if the menu is open is not, so why not continue to use the menu state value to determine if the menu is open or not? Something like this:
const openNav = () => {
setMenu(!menu);
};
return (
<nav id="nav-wrapper" className={menu ? "nav-open" : ""}>
// etc.
);
Basically any time you're trying to directly manipulate the DOM in React, take a step back and try to find a way to manage that via state instead. Direct DOM manipulation in React almost always leads to bugs unless you really know what you're doing under the hood in the framework.
When I click on the image I want that it is automatically scrolled to the component which is then displayed. I tried with anchor tags, but it's not working (I believe due to the fact that the component is hidden and at the same time when it is shown it should be scrolled to it ) , useRef - I get the error 'not defined' (I believe same reason as above).
Component is displyed onClick, but it does't scroll to the view-port of the user. Pls help, I'm out of the ideas :/
const WebContent = () => {
const [hidden, setHidden] = useState(false)
return (
<div>
<img onClick={() => setHidden(true)} src={first}/>
<div>
{hidden && <MyComponent/>}
</div>
</div>
)}
Your intuition is probably right that MyComponent is not yet mounted when you try to scroll to it. A simple way to do this would be to have MyComponent scroll itself into view when it mounts, if that's the behavior you're looking for.
const MyComponent = () => {
const ref = React.useRef(null);
useEffect(() => {
if (ref.current) ref.current.scrollIntoView();
}, [ref]);
return (
<div ref={ref}>
NOW YOU SEE ME
</div>
);
};
export default MyComponent;
One (hacky?) idea is add the ref to the surrounding div of the hidden content:
this.scrollHere = React.useRef(null);
...
return (
<div style={{ minHeight: 1 }} ref={this.scrollHere}>
{hidden && <div>My Hidden Component</div>}
</div>
)
Then you can run a function onClick, which sets hidden to true (which by the way is kinda irritating. Maybe just use "shown" as a quick improvement) and also lets the ref scrollIntoView:
const showAndScroll = () => {
setHidden(true);
this.scrollHere.current.scrollIntoView({
behavior: "smooth"
});
};
The minHeight has to be placed on the div since it is at height of 0 first and this messes with the scroll function (it scrolls below the hidden content).
See working example here.
I've been struggling with this issue lately.
I'm not sure if it has any connection to "sync/async" functions in JS. If it does, I would be more then thankful to understand the connection.
I've been making a simple component function:
There's a button "next","back" and "reset". Once pressing the matching button, it allows moving between linkes, according to button's type.
The links are an array:
const links = ["/", "/home", "/game"];
Here is the component:
function doSomething() {
const [activeLink, setActiveLink] = React.useState(0);
const links = ["/", "/home", "/game"];
const handleNext = () => {
setActiveLink((prevActiveLink) => prevActiveLink+ 1);
};
const handleBack = () => {
setActiveLink((prevActiveLink) => prevActiveLink- 1);
};
const handleReset = () => {
setActiveLink(0);
};
return (
<div>
<button onClick={handleReset}>
<Link className = 'text-link' to = {links[activeLink]}> Reset</Link>
</button>
<button onClick={handleBack}>
<Link className = 'text-link' to = {links[activeLink]}>Back</Link>
</button>
<button onClick={handleNext}>
<Link className = 'text-link' to = {links[activeLink]}>Next</Link>
</button>
</div>
When I'm trying to put the activeLink in the "to" attribute of Link, it puts the old value of it. I mean, handleNext/ handleReset/handleBack happens after the link is already set; The first press on "next" needs to bring me to the first index of the links array, but it stayes on "0".
Is it has to do something with the fact that setActiveLink from useState is sync function? or something to do with the Link?
I would like to know what is the problem, and how to solve it.
Thank you.
Your Links seem to be navigating to a new page?
If so, the React.useState(0) gets called each time, leaving you with the default value of 0.
Also your functions handleNext and handleBack aren't called from what I can see.
I am trying to render an array of elements in react so that when you hover over an element it then filters the array of elements and re-renders with just the hovered element. Keeping position is buggy but not the worry for now.
It works but when the hover element is rendered it has no content
const [show, setShow] = useState(false);
const cards = [{key:"1",title:"elem1",content:"..."},{key:"2",title:"elem2",content:"..."},{key:"3",title:"elem3",content:"..."}]
const tempCards = []
My hover function returns
tempCards = [{key:"1",title:"elem1"}];
/// temp function allowing to work from button click
const changeShow = () => {
tempCards = cards.filter(item => item === cards[0]);
console.log(tempCards[0])
setShow(!show)
}
This is all working as it should but then when the element loads on the page no content shows
{show ? cards.map((item)=> {
return <CardSpinLeft
onClick={toggle}
key={item.key}
id={item.key}
image={item.img}
name={item.title}
title={item.title}
paragraph={item.content}
/>
}):
<CardSpinLeft
onClick={toggle}
key={tempCards.key}
id={tempCards.key}
image={tempCards.img}
name={tempCards.title}
title={tempCards.title}
paragraph={tempCards.content}
/>
}
not receiving any error's I even tried making the filter function async/await thinking its a loading issue. Only thing I can think is that react is pre-loading the content before it is there?