loading...
Cover image for Part 2: Build This Cool Dropdown Menu with React, React Router and CSS

Part 2: Build This Cool Dropdown Menu with React, React Router and CSS

cooljasonmelton profile image Jason Melton Updated on ・6 min read

After my last blog about building a ~cool~ dropdown menu, I got a suggestion to write a part 2.

Andrew Bone pointed out, “Generally, when making a component, the aim is to make it reusable and simple enough that it rarely needs to be revisited.”

I took this to heart. In the following, I attempt to refactor my dropdown menu to be just this: reusable and simple.

demo gif

jump to the code on GitHub

Tutorial

The goal: take the previous dropdown menu and rewrite the code so it can use data from an interchangeable JSON object to create a dropdown with the same structure and styles as the original.

Table of Contents

  • Preliminary Junk
  • Mapping a JSON Object
  • Dynamic Styles
  • Dynamic Routes
  • Conclusion

Preliminary Junk

To start, I duplicated the original repository and created a JSON file with some dummy data.

json file

Mapping a JSON Object

To make the component dynamic, I use map(). Map is common in React apps as it can transform indefinitely sized array into JSX. This abstracts components making them flexible and reusable.

The original Menu component has a return that looks like this:

    return (
        <div className="Menu">
            <div className={"m-item m-logo"}
                onClick={() => setOpenMenu(!openMenu)}>
                Menu
            </div>
            <div className={setClassNames(1)}
                onClick={() => pushToRoute("/dashboard")}>
                Dashboard
            </div>
            <div className={setClassNames(2)}
                onClick={() => pushToRoute("/settings")}>
                Settings
            </div>
            <div className={setClassNames(3)}
                onClick={() => pushToRoute("/")}>
                Sign out
            </div>
        </div>
  );

You can see, each item is spelled out with its own div and properties. In the refactored version, I define a cookie cutter menu item and map() each menu item from JSON into it.

The new return looks like this:

    return (
        <div className="Menu">
            <div className={"m-item m-logo"}
                onClick={() => setOpenMenu(!openMenu)}>
                Menu
            </div>

            {renderMenuItems(data)}

        </div>
  );

The first menu item remains in its own div. This item acts like a button to open the dropdown. It displays when the menu is closed and the other menu items hide behind it.

Beneath that, I call the renderMenuItems() function which takes a JSON object as a parameter.

renderMenuItems() is complicated. I will show the whole function and then explain it piece by piece.

    // render each menu item after Menu button clicked
    const renderMenuItems = data => {    
        const colorArr = ["#9b5de5", "#f15bb5", "#00BBF9"];

        let colorCounter = -1;
        return data.menu.map((item, index) => {

            // if counter is over 2, resets to 0
            // for colorArr bracket notation to get sequence of colors
            colorCounter < 2 ? colorCounter++ : colorCounter = 0

            // dynamic styles for each menu item 
            const itemStyle = {
                "top": `${index * 1.8}em`,
                "backgroundColor": colorArr[colorCounter]
            }

            return (
                <div className="m-item"
                    key={item.id}
                    style={openMenu ? itemStyle : null}
                    onClick={() => pushToRoute(item.route)}>
                    {item.name}
                </div>
            )
        })
    }

I will explain colorArr, colorCounter, and itemStyle in the next section about Dynamic Styles.

First, notice this line:

        return data.menu.map((item, index) => {

I am returning a map() of data, the JSON object parameter. map() runs a callback on each item in an array, returning the result of that function in a new array.

map() can take two parameters. The first parameter is the item from the array. I labeled that item. The second is each item’s index, labeled index.

Now this part:

            return (
                <div className="m-item"
                    key={item.id}
                    style={openMenu ? itemStyle : null}
                    onClick={() => pushToRoute(item.route)}>
                    {item.name}
                </div>
            )

I return dynamic JSX for each item in the map(). These will be the divs of our menu items. Each item has an id, name, and route.

I give each div the m-item classname, unchanged from the original. They get an onClick event that triggers pushToRoute(). Also the same as the original except the parameter is in the JSON object as route. Each gets a key of the JSON’s id. Finally, I display the JSON object’s name as text in the div.

For reference, here's one of the JSON menu items:

      {
        "id": "001",
        "name": "Dashboard",
        "route": "/dashboard"
      }

Dynamic Styles

CSS is responsible for the dropdown animation and styling. In my original menu, I use a function called setClassNames() to add classnames to the items. Then, I spell out an individual classname for each item including the color and length I wanted each to drop to.

Add classnames to trigger transitions:

    // parameter num corresponds to .open-# classes
    // is assigned when Menu clicked triggering animated dropdown
    const setClassNames = num => {
        const classArr = ["m-item"];
        if (openMenu) classArr.push(`open-${num}`)
        return classArr.join(' ')
    }

The added classnames:

.open-1{
    top: 1.8em;
    background-color: #9b5de5;
}
.open-2{
    top: 3.6em;
    background-color: #f15bb5;
}
.open-3{
    top: 5.4em;
    background-color: #00BBF9;
}

While this works, it is not easily reusable. Not only do I have to manually spell out each new open-# for each additional item, I also use a lot of extra lines of code.

Since I am using map() on the new menu items, I can work out the styles as I go. There are two parts to the CSS that I need to include:

  1. A top set to a size of 1.8em times the number item it is on the list (1.8, 3.6, 5.4, 7.2, etc.).
  2. One of three color hexes set as a background-color (#9b5de5, #f15bb5, #00BBF9).

Take a look at renderMenuItems one more time.

    // render each menu item after initial Menu button
    const renderMenuItems = data => {    
        const colorArr = ["#9b5de5", "#f15bb5", "#00BBF9"];

        let colorCounter = -1;
        return data.menu.map((item, index) => {

            // if counter is over 2, resets to 0
            // for colorArr bracket notation to get sequence of colors
            colorCounter < 2 ? colorCounter++ : colorCounter = 0

            // dynamic styles for each menu item 
            const itemStyle = {
                "top": `${index * 1.8}em`,
                "backgroundColor": colorArr[colorCounter]
            }

            return (
                <div className="m-item"
                    key={item.id}
                    style={openMenu ? itemStyle : null}
                    onClick={() => pushToRoute(item.route)}>
                    {item.name}
                </div>
            )
        })
    }

React allows you to add styles to an element as an object with properties written in the camelcase syntax of JavaScript.

Notice in itemStyle, I set a top size. I use the map() index parameter to dynamically increase em size as map() iterates through the JSON.

The background-color is a little trickier. I set up an array called colorArr with the 3 color hexes. To access these, I set up a counter called colorCounter that I use to access the hexes.

The colorCounter is initially set to -1. To contially iterate through 0, 1, and 2 until map() finishes, I coded this ternary:

            // if counter is over 2, resets to 0
            // for colorArr bracket notation to get sequence of colors
            colorCounter < 2 ? colorCounter++ : colorCounter = 0

If the counter is less than 2, I add 1. If it is over 2, I reset the counter to 0. Thus, the counter will run 0, 1, 2, 0, 1, 2… for as long as the map() goes.

Check out Andrew Bone's comment for a more succinct way of getting the colorCounter number sequence.

In itemStyle, I set the “backgroundColor” to colorArr[colorCounter]. Thus the colors appear in a sequence.

Finally, I need to add the top and background-color properties to the items only when the first menu item is clicked.

As previously, when clicked, the top menu item toggles openMenu, a useState hook, between true and false.

I give each div a style property:

    style={openMenu ? itemStyle : null}

I use a ternary here to return the object containing the new top and background-color if openMenu is true. If false, it receives null.

Dynamic Routes

The final piece of this is to go back to my Switch statement in App.js and render the routes dynamically as well.

I can map() the same JSON object in order to set up the corresponding route of each menu item.

const App = () => {
  return (
    <BrowserRouter>
      <div className="App">
        {/* dropdown menu */}
        <Menu/>
        {/* routes */}
        <Switch>
          {/* map same data as dropdown to 
            create route for each item */}
          {data.menu.map(item =>{
            return(
              <Route key={item.id}
                exact path={item.route} 
                component={null} />
            )
          })}
        </Switch>
      </div>
    </BrowserRouter>
  );
}

Again, I'm missing the actual components of the the app that this menu would be applied to. If they were available, I could alter the JSON to include the component names or maybe to set up a lookup table that corresponds components to the IDs in the JSON.

Conclusion

It was great to revisit my code and improve on my initial ideas. Again, thanks to Andrew Bone for challenging me to try this. I feel like I created a much more flexible, reusable tool this second time around.

If you have any feedback or suggestions, please reach out. Comment or email me at jason.melton2@gmail.com. Regardless, thanks for reading. Best, Jason.

Discussion

pic
Editor guide
Collapse
link2twenty profile image
Andrew Bone

Very nice! That's some production ready code. Having an ID to use as the key is a nice touch and will speed up potential re-renders 😊

Do you know about the Modulus (Remainder) operator? Using it you can get rid of colorCounter.


As the name suggests it give you the remainder of the division. For instance:

0 % 3 = 0
3 goes in 0 0 times neatly with a remainder of 0

1 % 3 = 1
3 goes in 1 0 times neatly with a remainder of 1

2 % 3 = 2
3 goes in 2 0 times neatly with a remainder of 2

3 % 3 = 0
3 goes in 3 1 time neatly with a remainder of 0

4 % 3 = 1
3 goes in 4 1 time neatly with a remainder of 1

and so on.


This means we could get the colour by doing some thing like this 😁

// dynamic styles for each menu item 
const itemStyle = {
  "top": `${index * 1.8}em`,
  "backgroundColor": colorArr[index % colorArr.length]
}
Collapse
cooljasonmelton profile image
Jason Melton Author

Great tip! I've used the Modulus operator before but not in this way. That's a super useful trick!

Collapse
andrewbaisden profile image
Andrew Baisden

Nice like it.

Collapse
cooljasonmelton profile image