useEffect not applying classNames - reactjs

I'm currently working on a project using Next JS, where I seem to have encountered an issue with React. Here is the simplified version of my code. I did try to replicate the issue in codesandbox but I couldn't. I'll keep trying though and if I can, I'll update this post with the link.
const Nav = React.forwardRef<
HTMLDivElement,
{ className?: string; disableAnimation?: boolean }
>((props, ref) => {
const navWrapperRef = useRef<HTMLDivElement>(null);
const navItemsRef = useRef<HTMLSpanElement[]>([]);
useEffect(() => {
const path = window.location.pathname;
if (path === "/") navItemsRef.current[0].classList.add("nav-active");
else if (path.includes("/packages"))
navItemsRef.current[1].classList.add("nav-active");
else if (path.includes("/bhutan"))
navItemsRef.current[2].classList.add("nav-active");
}, []);
return (
<nav className={props.className || "nav"} ref={navWrapperRef}>
<div className="nav-container">
<ul className="nav-ul">
{navLinks.map((link) => (
<button
key={uniqueId(`nav-links-${new Date().getUTCDate()}`)}
data-name={link.name}
className="nav-links"
>
<span
ref={(el) => navItemsRef.current.push(el as HTMLSpanElement)}
className="nav-span"
>
<Link href={link.href}>{link.name}</Link>
</span>
</button>
))}
</ul>
</div>
</nav>
);
});
My objective here is to implement a navigation component without the use of states. I'd like to render out the current active navigation link on the initial page load using the empty dependency array as for the useEffect hook. But I can't seem to get it to work.
My desired output is the following on page load:
The output I get:
However, if I remove the dependency array altogether then all seems to work fine as expected. But if I am not wrong I think this causes performance issues as it re-renders each and every time if there are any other state changes. Any help would be greatly appreciated!

The contents of your useEffect hook will run once on mount and whenever its dependencies change.
As this is reliant on what you have defined as path, I'd move it out of the useEffect and add it as a dependency.
Update: you will have to use next/router's useRouter hook instead of the window directly when working with next.
Demo here.
const { asPath } = useRouter();
useEffect(() => {
if (asPath === "/") navItemsRef.current[0].classList.add("nav-active");
else if (asPath.includes("/packages"))
navItemsRef.current[1].classList.add("nav-active");
else if (asPath.includes("/bhutan"))
navItemsRef.current[2].classList.add("nav-active");
}, [asPath]);

Related

React UseRef is undefined using a wrapper

I'm following this example in order to achive dynamically-created elements that can be printed using react To Print.
I have the following code (showing sections to keep this question as clean as possible):
/*This section is loaded after a user has been selected from a select input*/
{rows?.map((row,index) => (
<PrintHojaVacaciones key={index} vacacion={row}/>
))}
const PrintHojaVacaciones = ({vacacion}) => {
const componentRef = useRef();
return (
<div>
{vacacion.id}
<ReactToPrint
trigger={() => {
<SqIconButton tip={`Imprimir (Aprobada por ${vacacion.aprobadapor})`}
color={"success"}
disableElevation={true}>
<><BsIcons.BsHandThumbsUpFill/><AiIcons.AiFillPrinter/></>
</SqIconButton>
}}
content={() => componentRef.current}
/>
<Diseno_PrintHojaVacaciones ref={componentRef} value={vacacion}/>
</div>
)
}
export default PrintHojaVacaciones
const Diseno_PrintHojaVacaciones = React.forwardRef((props,ref) => {
const { value } = props;
return (
<div ref={ref}>{value.aprobadapor}</div>
);
});
export default Diseno_PrintHojaVacaciones;
Thing is, useRef is undefined. I have been trying with CreateRef as well, with the same result. I also tried to "move" my code to the codesandbox above displayed and it works well, but in my own application, it returns undefined. The whole useRef is new to me and I don't understand it well yet, so any help would be appreciated.
The route is being called using Lazy loading from react router (I don't know if this could be the culprit)
I don't know exactly what I did to make it work, I really have no idea, but my Trigger now looks like this and it is now working (not sure if I'm missing something else). The difference is that the function is not inside brackets after the =>.
trigger={() =>
<SqIconButton tip={`Imprimir (Aprobada por ${vacacion.aprobadapor})`}
color={"success"}
disableElevation={true}>
<><BsIcons.BsHandThumbsUpFill/><AiIcons.AiFillPrinter/></>
</SqIconButton>
}

How do I conditionally render elements based off of path react.js

So I'm creating a blog app with react.js and ruby on rails. I have these buttons that are in my nav component that I need to conditionally render based off of the path the user is in. I'm using useLocation to accomplish this and almost have it where I want it. The problem I'm having is getting them to render in the three main paths where posts can be seen ('/general', '/resources', & '/events') while hiding them when a user goes into a post to view the comments. The buttons will show up in those paths if I remove the /path/id but as I stated I need them not to render in the /path/id just the /path. How can I accomplish this?
const [getAllPosts, setGetAllPosts] = useState([])
useEffect(() => {
const fetchData = async () => {
const res = await fetchAllPosts()
setGetAllPosts(res.filter(post => {
return post.category.title === 'general' || post.category.title === 'resources' || post.category.title === 'events'
}))
}
fetchData()
}, [])
return (
{getAllPosts.forEach((generalPost, resourcePost, eventsPost) => {return location.pathname === '/user' || location.pathname === '/about' || location.pathname === '/create' || location.pathname === `/general/${generalPost.id}` || location.pathname === `/general/${resourcePost.id}` || location.pathname === `/general/${eventsPost.id}` ? (<></>) : (<div className={open ? 'RegisterContainerMobileOpen' : 'RegisterContainerMobile'}>
{currentUser ? (
<button onClick={handleLogout} className='logoutmobile'>Logout</button>
) : (
<Link to='/user' className='resgisterlinkmobile'>
<button className='registermobile'>Signin/Signup</button>
</Link>
)}
</div>)})})
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
If you need to see the rest of my code just let me know. I've been working day and night on this with little sleep so I feel I am missing something simple.
Edit: I've also tried using .map() but get back multiple instances of the same button since map returns a new array. I've looked into React.memo in my research, but I'm not fully sure how I'd use that or even if it would be a fix to the mapping issue
Note: after reading more documentation on React.memo it does not seem like that would help
Without going too deeply into the task, try to add return and instead of forEach use map. Remember that forEach returns nothing, unlike map.
So it could look like this:
useEffect(() => {
...
}
return getAllPosts.map(...

What is the right place to call a function before render in React?

I have some issue on understandsing the lifecycle in React, so iam using useEffects() since i do understand that it was the right way to call a method before the component rendered (the replacement for componentDidMount ).
useEffect(() => {
tagSplit = tagArr.split(',');
});
And then i call tagSplit.map() function on the component, but it says that tagSplit.map is not a function
{tagSplit.map((item, index) => (
<div className="styles" key={index}>
{item}
</div>
))}
Is there something wrong that i need to fix or was it normal ?
useEffect runs AFTER a render and then subsequently as the dependencies change.
So yes, if you have tagSplit as something that doesn't support a map function initially, it'll give you an error from the first render.
If you want to control the number of times it runs, you should provide a dependency array.
From the docs,
Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
This article from Dan Abramov's blog should also help understand useEffect better
const React, { useState, useEffect } from 'react'
export default () => {
const [someState, setSomeState] = useState('')
// this will get reassigned on every render
let tagSplit = ''
useEffect(() => {
// no dependencies array,
// Runs AFTER EVERY render
tagSplit = tagArr.split(',');
})
useEffect(() => {
// empty dependencies array
// RUNS ONLY ONCE AFTER first render
}, [])
useEffect(() => {
// with non-empty dependency array
// RUNS on first render
// AND AFTER every render when `someState` changes
}, [someState])
return (
// Suggestion: add conditions or optional chaining
{tagSplit && tagSplit.map
? tagSplit.map((item, index) => (
<div className='styles' key={index}>
{item}
</div>
))
: null}
)
}
you can do something like this .
function App() {
const [arr, setArr] = useState([]);
useEffect(() => {
let tagSplit = tagArr.split(',');
setArr(tagSplit);
}, []);
return (
<>
{arr.map((item, index) => (
<div className="styles" key={index}>
{item}
</div>
))}
</>
)
}
Answering the question's title:
useEffect runs after the first render.
useMemo runs before the first render.
If you want to run some code once, you can put it inside useMemo:
const {useMemo, Fragment} = React
const getItemsFromString = items => items.split(',');
const Tags = ({items}) => {
console.log('rendered')
const itemsArr = useMemo(() => getItemsFromString(items), [items])
return itemsArr.map((item, index) => <mark style={{margin: '3px'}} key={index}>{item}</mark>)
}
// Render
ReactDOM.render(<Tags items='foo, bar, baz'/>, root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
For your specific component, it's obvious there is no dilema at all, as you can directly split the string within the returned JSX:
return tagArr.split(',').map((item, index) =>
<div className="styles" key={index}>
{item}
</div>
)
But for more complex, performance-heavy transformations, it is best to run them only when needed, and use a cached result by utilizing useMemo

useEffect not cleaning up after redirect

Let me explain my problem I have been solving for all day.
I have a site with header which has of course by react-router links to other pages (home, projects, about, services, contact).
Have a Project component which is in '/projects' page and '/' (home) page.
I want to make a simple animation in Project.js component which depends if there is a 'vertical' or there is not this props. Clearly -> in '/projects' I want to do that animation on scroll - in other pages not.
Tried to do that by add if statement in useEffect but it's not working, get me an error 'cannot read property 'style' of null ref.current.style.transform = `translateY(${window.scrollY * -0.35}px)`;
This problem is showing up when I am changing pages in header i.eg. I am in '/projects' scrolling and is ok animation is working then go to '/' and when scroll got error I have showed above.
It is like my if statement is not working and when I am in '/' which Project component has props vertical={false} is making animation on scroll when I don't want to do that.
What I want? I want do make an animation using useEffect only if component has a props 'vertical' like this:
Project.js component code:
const Project = ({ image, className, vertical }) => {
const ref = useRef(null);
const [isVertical, setIsVertical] = useState(vertical);
useEffect(() => {
console.log('component did mount');
isVertical
? window.addEventListener('scroll', () => {
ref.current.style.transform = `translateY(${window.scrollY * -0.35}px)`;
})
: console.log('non-vertical');
}, [isVertical]);
useEffect(() => {
return () => console.log('unmount');
});
return <StyledProject image={image} className={className} vertical={vertical} ref={ref} />;
};
in home '/':
{images.map(({ image, id }) => (
<Project key={id} image={image} />
))}
in '/projects':
{images.map(({ image, id }) => (
<StyledProject vertical image={image} key={id} />
))}
when I am in the path '/projects' and go to another path got error.
It is like after being in '/projects' it is saving all statements was but I want on every page reset useEffect and ref.current
Please help me, I can't go further since I don't fix this.
Thanks in advance.
Main problem is that you are not removing event listener when component unmounts.
Here you can see an example how to do it.

onClick event not called when clicking

React onClick event not working when clicking on glyphicon.
const SortableItem = SortableElement(({value, parentId}) =>
<ListGroupItem bsStyle='success' style={{width:'300px',textAlign:'left'}}>
{value}
{parentId === null && <span className='glyphicon glyphicon-remove' style={{float:'right',width:'10px',height:'10px'}}
onClick={e => {e.preventDefault(); console.log('yes')}}/>}
</ListGroupItem>
);
I ran into something similar. My onClick events on my <a> elements were not getting triggered when a user clicked them.
This is what I was doing wrong and maybe you are doing the same mistake as what I was doing. Without more context, it's impossible to diagnose what your actual problem is.
(code is basically saying, when I click the background, stop propagation of the click)
// Example of what I was doing WRONG
const Dialog = ({ onClose, children }) => {
const $overlay = React.useRef(null)
// on mount
React.useEffect(() => {
const handleBackgroundClick = (e) => {
e.stopPropagation() // <<-- This was the line causing the issue
onClose()
})
$overlay.current?.addEventListener('click', handleBackgroundClick)
// on unmount
return () => {
$overlay.current?.removeEventListener('click', handleBackgroundClick)
}
}, [])
return (
<div className="Dialog" ref={$overlay}>{children}</div>
)
}
Basically what I'm saying is this:
Do not use event.stopPropagation() directly on DOM elements in React
The events need to be able to bubble all the way to the top level or in-built React event's will stop working.
I ended up getting around the issue by adding an extra div inside the dialog to act as the overlay.
// How I fixed the issue
const Dialog = ({ onClose, children }) => {
return (
<div className="Dialog">
<div className="Dialog__overlay" onClick={()=> onClose()}></div>
<div className="Dialog__content">{children}</div>
</div>
)
}
Note: These are simplified examples to demonstrate a point. The exact code used was more complex. Using this code as displayed here would cause accessibility issues on your website.

Resources