Filter Dropdown Menu

Build a responsive dropdown menu for applying filters to a list.


Environment

Use the environment you are most comfortable with. Because the React docs site suggests using a full-stack React framework, these challenges use the most popular of these frameworks, Next.js. You can run

npx create-next-app@latest

from your terminal to set up a new Next.js project, then use npm run dev to develop locally and inspect the DOM from your browser when debugging. I opt to use App Router, made stable in Next.js 13.4 over the Pages Router. I also use tailwindcss for simple inline styling.

Specification

In this challenge, your goal is to create a filter dropdown component using React. The starter code provides a basic structure for the FilterDropdown component. Below are a few images of the finished component followed by the challenge requirements.

The component after opening the dropdown
The component after opening the dropdown
The component after enabling two filters
The component after enabling two filters

Filter Options

The dropdown should display a list of filter options. Each option is represented by a string, and the initial set of options is provided in the FILTERS array.

Toggle Filter

Clicking on a filter option should toggle its state between enabled and disabled. The enabled state should be visually distinguishable, for example, by changing the background color.

Dropdown Toggle

Clicking on the main dropdown area should toggle the visibility of the filter options. When the filter options are visible, the dropdown should display either "Filtering" if any filter is enabled or "Add Filter" if no filters are enabled.

Visual Feedback & Styling

Provide visual feedback to indicate the active state of the filter options. For instance, when a filter is enabled, the background color can change to indicate its status. The solution below also includes color changes when the user hovers over a filter.

Starter Code

Below is a small skeleton of the application to get you started.

app/page.tsx

import FilterDropdown from './FilterDropdown'

export default function Home() {
  return (
    <div className='flex justify-center h-screen w-screen pt-10'>
      <FilterDropdown/>
    </div>
  )
}

app/FilterDropdown.tsx

'use client'

import { useMemo, useState } from "react"

const FILTERS = [
  'Plays a Sport',
  'Likes Pizza',
  '> 1.75m Tall',
  '< 35 Years Old'
]

export default function FilterDropdown(){

  // YOUR CODE HERE

}

Solution

Below is the entire solution code for the component followed by a short explanation of how it works.

app/FilterDropdown.tsx

'use client'

import { useMemo, useState } from "react"

const FILTERS = [
  'Plays a Sport',
  'Likes Pizza',
  '> 1.75m Tall',
  '< 35 Years Old'
]

export default function FilterDropdown(){
  const [open, setOpen] = useState(true)
  const [filters, setFilters] = useState(() => {
    return FILTERS.map((str) => {
      return {
        name: str,
        enabled: false
      }
    })
  })

  const clickFilter = (name: string) => {
    setFilters(filters.map((item) => {
      if (item.name == name) return {...item, enabled: !item.enabled}
      else return {...item}
  }))
  }

  const filtering = useMemo(
    () => filters.find((item) => item.enabled) != undefined, 
    [filters])


  return (
    <div>
      <div 
        className="w-56 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-inset hover:bg-gray-100"
        onClick={() => setOpen(!open)}>
        {filtering ?  'Filtering':'Add Filter'}
      </div>
      <div className={`mt-2 origin-top-right rounded-md bg-white shadow-lg py-1 ${open ? '' : 'hidden'}`}>
          {filters.map((item) => 
            <div 
              key={item.name} 
              className={`text-gray-700 block px-4 py-2 text-sm ${item.enabled ? 'bg-violet-400 hover:bg-violet-300': 'hover:bg-gray-100'}`}
              onClick={() => clickFilter(item.name)}>
                {item.name}
            </div>
          )}
      </div>
    </div>
  )
}

Let's break down the solution code and understand its functionality:

The component uses two pieces of state. The open state is a boolean flag indicating whether the filter options dropdown is open or closed. The filters state is an array of objects, each representing a filter option. Each object has a name property (the filter text) and an enabled property indicating whether the filter is currently active.

The clickFilter function toggles the enabled state of a filter when it is clicked. It uses the setFilters function to update the state by mapping over the existing filters array. Importantly, map returns a new object. If the element to be updated in the filters array was changed directly and array passed to setFilters was the same as the previous array, react would assume the state was not changed, and the next render would not reflect the change.

The filtering variable is a memoized value that indicates whether any filter is currently enabled. It relies on the useMemo hook to compute this value only when the filters state changes.

The FilterDropdown component renders a div containing two nested divs. The first div serves as the main dropdown area. It displays "Filtering" if any filter is active and "Add Filter" otherwise. Clicking on it toggles the visibility of the filter options. The second div contains the actual filter options, each represented by a div. The styling and interactivity are applied using Tailwind CSS classes.

As a follow-up challenge, display the list of active filters in a search bar instead of the "Filtering" text currently in place. Allow the user to remove them by clicking on them in the search bar.


useMemo
useState