Search code examples
javascriptcssreactjsbackground-imageuse-state

Changing background image using useState and onClick with React


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

Solution

  • Issues

    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.

    Solution

    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)}
    

    Code

    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;
    

    React Router

    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"
        }}
      >