Smart Dropdowns in React: Using useReducer and useRef for Outside Click Handling

chintanonweb - Oct 16 - - Dev Community

How to Create a Dropdown in React with Tailwind CSS Using useReducer and useRef

Creating dropdown menus in a React application can enhance user experience by providing a compact way to navigate and access additional information. In this guide, we will implement two dropdowns—one for notifications and one for user profile settings—using useReducer for state management and useRef to handle outside clicks to close the dropdowns. We will also incorporate Font Awesome icons for a polished look.

Introduction

In modern web development, managing user interfaces efficiently is crucial. React, combined with Tailwind CSS, provides a powerful toolkit for building responsive components. We will learn how to handle dropdown functionality in React, ensuring that clicking outside of a dropdown will close it, while maintaining the ability to open or close each dropdown independently.

What Are useReducer and useRef?

Before diving into the code, let’s understand the two React hooks we’ll be using:

  • useReducer: This hook is an alternative to useState for managing state in functional components. It is especially useful for managing complex state logic and multiple state variables. The useReducer hook takes a reducer function and an initial state, returning the current state and a dispatch function to update that state.

  • useRef: This hook provides a way to reference DOM elements directly. It is useful for accessing and manipulating elements without triggering re-renders. In our case, we will use useRef to check if a click happened outside the dropdown menus.

Step-by-Step Implementation

Step 1: Set Up the Project

First, ensure you have a React project set up with Tailwind CSS. If you don't have one, you can create it using Create React App:

npx create-react-app my-dropdown-app
cd my-dropdown-app
npm install tailwindcss
npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

Configure Tailwind by adding the following lines to your tailwind.config.js:

module.exports = {
  purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
  darkMode: false,
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Then, add the Tailwind directives to your index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Font Awesome

To use Font Awesome icons, you need to install the @fortawesome/react-fontawesome package:

npm install @fortawesome/react-fontawesome @fortawesome/free-solid-svg-icons
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the Navbar Component

In your src directory, create a new file named Navbar.tsx. This component will contain the dropdowns.

Implement the Navbar Code

Here’s the code for the Navbar component, which utilizes useReducer and useRef to handle dropdown states and outside clicks.

import React, { useRef, useEffect, useReducer } from "react";
import { Link } from "react-router-dom";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBell, faUser, faCaretDown } from '@fortawesome/free-solid-svg-icons';

interface IState {
  isProfileOpen: boolean;
  isNotificationOpen: boolean;
}

type Action =
  | { type: 'toggleProfile' }
  | { type: 'toggleNotification' }
  | { type: 'closeAll' };

function reducer(state: IState, action: Action): IState {
  switch (action.type) {
    case 'toggleProfile':
      return {
        isProfileOpen: !state.isProfileOpen,
        isNotificationOpen: false
      };
    case 'toggleNotification':
      return {
        isProfileOpen: false,
        isNotificationOpen: !state.isNotificationOpen
      };
    case 'closeAll':
      return {
        isProfileOpen: false,
        isNotificationOpen: false
      };
    default:
      return state;
  }
}

const Navbar: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, { isProfileOpen: false, isNotificationOpen: false });
  const profileDropdownRef = useRef<HTMLDivElement>(null);
  const notificationDropdownRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      const target = event.target as Node;

      if (
        (profileDropdownRef.current && !profileDropdownRef.current.contains(target)) ||
        (notificationDropdownRef.current && !notificationDropdownRef.current.contains(target))
      ) {
        dispatch({ type: 'closeAll' });
      }
    };

    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, []);

  const toggleProfileDropdown = (event: React.MouseEvent) => {
    event.stopPropagation();
    dispatch({ type: 'toggleProfile' });
  };

  const toggleNotificationDropdown = (event: React.MouseEvent) => {
    event.stopPropagation();
    dispatch({ type: 'toggleNotification' });
  };

  const closeDropdowns = () => {
    dispatch({ type: 'closeAll' });
  };

  const notificationItems = [
    { text: "New data received", time: "2 Days Ago" },
    { text: "New update available", time: "1 Day Ago" },
    { text: "Scheduled maintenance", time: "3 Days Ago" },
  ];

  const profileItems = [
    { label: "Profile", link: "#" },
    { label: "Settings", link: "#" },
    { label: "Logout", link: "#" }
  ];

  return (
    <header className="w-full bg-white shadow p-4 sticky">
      <nav className="flex justify-between items-center">
        <div className="text-2xl font-bold">DC Dashboard</div>

        <div className="flex items-center space-x-6">
          <input
            type="text"
            placeholder="Search"
            className="hidden md:block px-3 py-1 border border-gray-300 rounded-md focus:outline-none focus:ring focus:border-black-300 transition-all duration-300"
          />

          <div className="relative flex items-center">
            <button onClick={toggleNotificationDropdown} className="h-8 w-6 text-gray-500">
              <FontAwesomeIcon icon={faBell} />
              <span className="absolute top-0 right-0 bg-black text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">2</span>
            </button>

            <div
              ref={notificationDropdownRef}
              className={`absolute top-8 right-0 mt-4 z-20 w-96 bg-white rounded-xl shadow flex-col ${state.isNotificationOpen ? "" : "hidden"}`}
            >
              <h6 className="self-stretch px-5 py-2.5 text-black font-semibold">Notifications</h6>
              <ul className="pb-2 w-full">
                {notificationItems.map((item, index) => (
                  <li key={index} className="px-5 py-2.5 border-t border-black/10 hover:bg-gray-100 cursor-pointer" onClick={closeDropdowns}>
                    <div className="flex justify-between">
                      <p className="text-xs font-bold">{item.text}</p>
                      <p className="text-xs font-normal">{item.time}</p>
                    </div>
                  </li>
                ))}
              </ul>
            </div>
          </div>

          <div className="relative">
            <button className="flex items-center space-x-2" onClick={toggleProfileDropdown}>
              <FontAwesomeIcon icon={faUser} />
              <span className="hidden md:block font-medium">CodeWithChintan</span>
              <FontAwesomeIcon icon={faCaretDown} />
            </button>

            <div
              ref={profileDropdownRef}
              className={`absolute right-0 mt-4 w-48 bg-white shadow-md rounded-md z-10 ${state.isProfileOpen ? "" : "hidden"}`}
            >
              <ul className="py-2">
                {profileItems.map((item, index) => (
                  <li key={index} className="px-4 py-2 hover:bg-gray-100 cursor-pointer" onClick={closeDropdowns}>
                    <Link to={item.link}>{item.label}</Link>
                  </li>
                ))}
              </ul>
            </div>
          </div>
        </div>
      </nav>
    </header>
  );
};

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

Step 4: Integrate the Navbar in Your App

Open your App.tsx file and import the Navbar component to include it in your application layout.

import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Navbar from './components/Navbar';

const App: React.FC = () => {
  return (
    <Router>
      <Navbar />
      <main className="p-4">
        <h1 className="text-2xl font-bold">Welcome to DC Dashboard!</h1>
        {/* Other components and content */}
      </main>
    </Router>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Step 5: Style with Tailwind CSS

The provided classes from Tailwind CSS should already give a neat design. However, feel free to customize styles as needed.

Step 6: Test the Application

Start your application by running:


bash
npm start
Enter fullscreen mode Exit fullscreen mode

You should now see a navigation bar at the top of your application with functional dropdowns for notifications and user profile settings.

FAQ

1. How does the useReducer hook work in this example?

The useReducer hook allows us to manage the state of multiple dropdowns efficiently. We define a reducer function that takes the current state and an action to return the new state. This pattern is helpful for toggling dropdowns and handling the logic for closing all dropdowns at once.

2. Why use useRef?

We use useRef to reference the dropdown elements. This lets us check if a click event occurred outside these elements. If it does, we dispatch an action to close the dropdowns, ensuring a clean user experience.

3. Can I add more dropdowns?

Yes! You can extend the state in the reducer and add more dropdowns similarly. Just ensure each dropdown has its own ref and toggle function.

4. Is Tailwind CSS necessary for this implementation?

No, Tailwind CSS is not mandatory. You can style your dropdowns with any CSS framework or custom CSS styles, but Tailwind makes styling quicker and more responsive.

Conclusion

In this guide, you’ve learned how to create a functional dropdown menu in React using useReducer for state management and useRef for handling outside clicks. This approach provides a clean and efficient way to manage complex UI interactions, enhancing the overall user experience. Feel free to build upon this foundation and customize it to fit your application's needs!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .