I have been going in circles trying to make this work. My goal is to have a background image that displays behind a link after it is clicked. Each Link on my Navigation has its own image for when clicked. The first error I was experiencing was too many re-renders. I manipulated the code more, the error went away however the images are still not rendering onClick. Any help that can be provided would be greatly appreciated, I am still pretty new to React and would love to understand what I am doing wrong. Thank you!
import { Link } from 'react-router-dom'
import { useState } from 'react'
import Logo from '../../assets/images/Platform-Logo.svg'
import artistPaintBackground from '../../assets/images/Navigation-Artists-SVG.svg'
import eventsPaintBackground from '../../assets/images/Navigation-Events.svg'
import contactUsPaintBackground from '../../assets/images/Navigation-Contact-Us.png'
import homePaintBackground from '../../assets/images/Navigation-Home.png'
const navLinks = [
{
id: 0,
title: `HOME`,
path: `/`,
bgI: '',
},
{
id: 1,
title: 'EVENTS',
path: '/events',
bgI: '',
},
{
id: 2,
title: `ARTISTS`,
path: `/artists`,
bgI: '',
},
{
id: 3,
title: `CONTACT US`,
path: `/contactUs`,
bgI: '',
},
]
const Nav = (props) => {
const [navLinksBackgroundImage, setNavLinksBackgroundImage] = useState({
navLinks: [
{
id: 0,
title: `HOME`,
path: `/`,
bgI: '',
},
{
id: 1,
title: 'EVENTS',
path: '/events',
bgI: '',
},
{
id: 2,
title: `ARTISTS`,
path: `/artists`,
bgI: '',
},
{
id: 3,
title: 'CONTACT US',
path: `/contactUs`,
bgI: '',
},
],
})
const css = {
backgroundImage: `url(${navLinks.bgI})`,
width: '20%',
height: '100%',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
}
const addBgIHandler = () => {
setNavLinksBackgroundImage({
navLinks: [
{
id: 0,
title: `HOME`,
path: `/`,
bgI: ` ${homePaintBackground}`,
},
{
id: 1,
title: 'EVENTS',
path: '/events',
bgI: `${eventsPaintBackground}`,
},
{
id: 2,
title: `ARTISTS`,
path: `/artists`,
bgI: `${artistPaintBackground}`,
},
{
id: 3,
title: 'CONTACT US',
path: `/contactUs`,
bgI: `${contactUsPaintBackground}`,
},
],
})
if (navLinks.id === 0) {
return `${homePaintBackground}`
} else if (navLinks.id === 1) {
return `${eventsPaintBackground}`
} else if (navLinks.id === 2) {
return `${artistPaintBackground}`
} else if (navLinks.id === 3) {
return `${contactUsPaintBackground}`
} else {
return null
}
}
return (
<div>
<AppBar position='static' className={classes.navBar}>
<Toolbar>
<Container maxWidth='xl' className={classes.navDisplayFlex}>
<Link to='/'>
<img className={classes.imageLogo} src={Logo} />
</Link>
<Hidden smDown>
<ThemeProvider theme={theme}>
<List
component='nav'
aria-labelledby='main-navigation'
className={classes.navDisplayFlex}
>
{navLinks.map(({ title, path, bgI }) => (
<Link
active={bgI}
to={path}
key={title}
value={bgI}
className={classes.linkText}
onClick={addBgIHandler}
style={css}
>
<ListItem disableGutters={true}>
<ListItemText primary={title} />
</ListItem>
</Link>
))}
</List>
</ThemeProvider>
</Hidden>
<Hidden mdUp>
<Dropdown navLinks={navLinks} />
</Hidden>
</Container>
</Toolbar>
</AppBar>
</div>
)
}
export default Nav
There are a few errors in this code. The first one I see is on this line:
backgroundImage: `url(${navLinks.bgI})`,
navLinks
is an array
, so it doesn't have a bgI
property. The individual elements of the array are objects with a bgI
property.
The next confusing thing is the addBgIHandler
function. This function calls setNavLinksBackgroundImage
and updates the state to one where every link has an image for its bgI
. It then goes through a bunch of if
/else
cases and returns a single bgI
. But this returned value is never used anywhere.
In general you want to store the minimal amount of information in state that you need. If something never changes then it doesn't need to be in state. The only changing information that we need to know here is which link was clicked?
I'm going to keep your navLinks
array defined outside of the component, but change it so that it includes the bgI
for every image. We want to know the bgI
for each link, but that doesn't mean we will show the bgI
.
We will use a state to tell us which link (if any) is the active link. I am using the id
but it doesn't matter. You could use the title
or the path
instead.
// Should the initial state be home? Or no active link? That's up to you.
const [activeLinkId, setActiveLinkId] = useState(0);
We will only show the background image on a link if it is the active link. Instead of a constant css
we will need to make it a function so that it can be different for each link. We are going to call this function from inside navLinks.map
, so we can pass in the arguments that we need. We will need the id
in order to compare it to the activeLinkId
state and the bgI
to set the background image if it's active.
I would recommend that you move the styles width: '20%', height: '100%',
which are always present into your classes
object and just handle the background image here.
const createCss = (id: number, bgI: string) => {
if (id === activeLinkId) {
return {
backgroundImage: `url(${bgI})`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat"
};
} else return {};
};
In my own code I would use a ternary operator instead of if
/else
but I want to keep this clear and readable.
In the Link
, we call the function to set the style
prop.
style={createCss(id, bgI)}
Our new addBgIHandler
is so simple that we can define it inline. When this link is clicked, we set the activeLinkId
to this id
.
onClick={() => setActiveLinkId(id)}
const navLinks = [
{
id: 0,
title: `HOME`,
path: `/`,
bgI: ` ${homePaintBackground}`
},
{
id: 1,
title: "EVENTS",
path: "/events",
bgI: `${eventsPaintBackground}`
},
{
id: 2,
title: `ARTISTS`,
path: `/artists`,
bgI: `${artistPaintBackground}`
},
{
id: 3,
title: "CONTACT US",
path: `/contactUs`,
bgI: `${contactUsPaintBackground}`
}
];
const Nav = (props) => {
// Should the initial state be home? Or no active link? That's up to you.
const [activeLinkId, setActiveLinkId] = useState(0);
const createCss = (id: number, bgI: string) => {
if (id === activeLinkId) {
return {
backgroundImage: `url(${bgI})`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat"
};
} else return {};
};
return (
<div>
<AppBar position="static" className={classes.navBar}>
<Toolbar>
<Container maxWidth="xl" className={classes.navDisplayFlex}>
<Link to="/">
<img className={classes.imageLogo} src={Logo} />
</Link>
<Hidden smDown>
<ThemeProvider theme={theme}>
<List
component="nav"
aria-labelledby="main-navigation"
className={classes.navDisplayFlex}
>
{navLinks.map(({ title, path, bgI, id }) => (
<Link
to={path}
key={title}
className={classes.linkText}
onClick={() => setActiveLinkId(id)}
style={createCss(id, bgI)}
>
<ListItem disableGutters={true}>
<ListItemText primary={title} />
</ListItem>
</Link>
))}
</List>
</ThemeProvider>
</Hidden>
<Hidden mdUp>
<Dropdown navLinks={navLinks} />
</Hidden>
</Container>
</Toolbar>
</AppBar>
</div>
);
};
export default Nav;
I've answered the question exactly as you've presented it, which is "how do I show an image on a link when it's clicked?" But I see that the Link
component is imported from react-router-dom
so I think that you aren't asking the right question. The question should be "how do I show an image for the current page link?" That question has a different answer which doesn't use state at all.
You can replace your Link
components with NavLink
.
A special version of the
<Link>
that will add styling attributes to the rendered element when it matches the current URL.
Isn't that what we want? With NavLink
we can ditch the useState
and the onClick
and the createCss
. Instead we set our background styles to the activeStyle
prop, which will only apply these styles when the link is active.
{navLinks.map(({ title, path, bgI }) => (
<NavLink
to={path}
key={title}
className={classes.linkText}
activeStyle={{
backgroundImage: `url(${bgI})`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat"
}}
>