5 Most Common Dropdown Use Cases Solved with React Downshift

Publikováno: 26.6.2018

Downshift is a library that helps you build simple, flexible, WAI-ARIA compliant enhanced input React components. Its major use case is for buildin...

Celý článek

Downshift is a library that helps you build simple, flexible, WAI-ARIA compliant enhanced input React components. Its major use case is for building autocomplete components but it can also be used to build dropdown components. In this post, we’ll walk through some common use cases solved with Downshift.

Prerequisites

To follow this tutorial, you need Node and NPM installed on your machine. A basic understanding of React will help you get the most out of this tutorial. If you don’t have Node.js installed, go to the Node website and install the recommended version for your operating system.

Setting up the application

We’ll make use of Create React App to create a simple React app with no build configuration. If you don’t have Create React App installed, run npm i create-react-app in your terminal to install it. Once you have it on your machine, run the following command to set up a new React project in a directory called downshift-examples and move to this new directory by running these commands:

$ create-react-app downshift-examples
$ cd downshift-examples

Once you’re in the downshift-examples directory, run the following command to install Downshift and some other packages:

$ yarn add downshift axios react-popper

Open the App.css file in the src folder and add the following styles:

    /* src/App.css*/

input {
      margin: 1rem;
      width: 20rem;
      padding: 1rem .5rem;
    }
    .downshift-dropdown {
      margin: 0 auto;
      width: 20rem;
      border: 1px solid whitesmoke;
      border-bottom: none;
    }
    .dropdown-item {
      padding: 0.5rem;
      cursor: pointer;
      border-bottom: 1px solid whitesmoke;
      font-size: 1rem;
      text-align: left;
    }
    .dropdown-button {
      padding: 0.6rem;
      border-radius: 3px;
      background: white;
      cursor: pointer;
    }
    .popper-div {
      flex: 1;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-top: 5rem;
    }
    .popper-item {
      padding: .5rem;
      border-bottom: 1px solid whitesmoke;
    }

With everything set up, let’s look at some basic Downshift concepts

Basic overview

When using Downshift, the only component we need is <Downshift />. We call the <Downshift /> component and pass it some props and it works its magic. Here are some of the most used props:

  1. onChange: This function is called when the user selects an item and the selected item has changed. It returns the selectedItem.
  2. itemToString: This function is used to determine the string value for the selected item which is used to compute the inputValue.
  3. inputValue: This represents the value the input field should have.
  4. getInputProps: This function returns the props you should apply to the input element that you render.
  5. getItemProps: This function returns the props you should apply to any menu item elements you render.
  6. isOpen: This is a boolean that indicates whether or not the menu is open.
  7. selectedItem: This represents the currently selected item input.
  8. render: This is where you render whatever you want to based on the state of Downshift. This function is called with an object.

You can check this link for the full list of props. Now let’s put this knowledge to use.

Basic select field

Our first Downshift use case is a simple select field. Go ahead and create a DownshiftOne.js file in the src folder in the root directory of your app. Add the following code to it:

    // src/DownshiftOne.js

import React from 'react'
import Downshift from 'downshift';

const books = [
      { name: 'Harry Potter' },
      { name: 'Net Moves' },
      { name: 'Half of a yellow sun' },
      { name: 'The Da Vinci Code' },
      { name: 'Born a crime' },
    ];

    const onChange = (selectedBook) => {
      alert(`your favourite book is ${selectedBook.name}`)
    }

    export default () => {
      return (
        <Downshift onChange={onChange} itemToString={books => (books ? books.name : '')}>
          {/* we'll insert a callback here */}
        </DownShift>
      )
    }

In the code above, we import React and Downshift and declare an array of books, an onChange function and also a functional component which returns the <Downshift/> component. In the <Downshift/> component, we pass the onChange and itemToString props. Inside the <Downshift/> component, we’ll pass other props to a callback and render our input field.

Next, we’ll pass the props we need in a callback to the <Downshift/> component. Update your functional component with the following:

    // src/DownshiftOne.js
    ...

export default () => {
      return (
        <Downshift onChange={onChange} itemToString={books => (books ? books.name : '')}>
          // pass the downshift props into a callback
          {({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex, selectedItem, getLabelProps }) => (
            <div>
              // add a label tag and pass our label text to the getLabelProps function
              <label style={{ marginTop: '1rem', display: 'block' }} {...getLabelProps()}>Choose your favourite book</label> <br />
              // add our input element and pass our placeholder to the getInputProps function
              <input {...getInputProps({ placeholder: "Search books" })} />
              // if the input element is open, render the div else render nothing
              {isOpen ? (
                <div className="downshift-dropdown">
                  {
                    // filter the books and return items that match the inputValue
                    books
                      .filter(item => !inputValue || item.name.toLowerCase().includes(inputValue.toLowerCase()))
                      // map the return value and return a div
                      .map((item, index) => (
                        <div
                          className="dropdown-item"
                          {...getItemProps({ key: item.name, index, item })}
                          style={{
                            backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                            fontWeight: selectedItem === item ? 'bold' : 'normal',
                          }}>
                          {item.name}
                        </div>
                      ))
                  }
                </div>
              ) : null}
            </div>
          )}
        </Downshift>
      )
    }

In the code snippet above, we passed our Downshift props as parameters to a callback:

{({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex, selectedItem, getLabelProps }) => ()}

In the callback, we add our input tag and pass it our getInputProps props:

<input {...getInputProps({ placeholder: "Search books" })} />

Next, we check if the input element is open. If it is, we return a div element containing our menu and return null if it’s not:

{ isOpen ? (<div className="downshift-dropdown">...</div>) : null }

Lastly, in the div element which we’re returning, we basically filter through our books array, returning only the items that include the inputValue. We then map through the filtered books and render them on the page. Note that we passed the getItemProps function to the div rendered in the map function. It returns the props that we applied while rendering the items.

Let’s import our component to the parent App component, and see our functional select field:

    // src/App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import DownshiftOne from './DownshiftOne'; // import the component

class App extends Component {
      render() {
        return (
          <div className="App">
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h1 className="App-title">Welcome to React</h1>
            </header>
            <DownshiftOne /> // render the component
          </div>
        );
      }
    }
    export default App;

Ensure your server is running by running npm start in your terminal. If you open http://localhost:3000 in your browser, you should see something like this:

Downshift with Axios

In our next example, we’ll use Downshift to create a search field for movies. In the src folder, create a DownshiftTwo.js file and add the following code:

    // src/DownshiftTwo.js

import React, { Component } from 'react'
import Downshift from 'downshift';
import axios from 'axios';

export default class DownshiftTwo extends Component {
      constructor(props) {
        super(props)
        this.state = {
          movies: []
        }
        this.fetchMovies = this.fetchMovies.bind(this)
        this.inputOnChange = this.inputOnChange.bind(this)
      }
      // onChange method for the input field
      inputOnChange(event) {
        if (!event.target.value) {
          return
        }
        this.fetchMovies(event.target.value)
      }
      // input field for the <Downshift /> component
      downshiftOnChange(selectedMovie) {
        alert(`your favourite movie is ${selectedMovie.title}`)
      }
      // method to fetch the movies from the movies API
      fetchMovies(movie) {
        const moviesURL = `https://api.themoviedb.org/3/search/movie?api_key=1b5adf76a72a13bad99b8fc0c68cb085&query=${movie}`;
        axios.get(moviesURL).then(response => {
          this.setState({ movies: response.data.results })
        })
      }
      render() {
        return (
          <Downshift onChange={this.downshiftOnChange} itemToString={item => (item ? item.title : '')}>
          // pass the downshift props into a callback
            {({ selectedItem, getInputProps, getItemProps, highlightedIndex, isOpen, inputValue, getLabelProps }) => (
              <div>
                // add a label tag and pass our label text to the getLabelProps function
                <label style={{ marginTop: '1rem', display: 'block' }} {...getLabelProps()}>Choose your favourite movie</label> <br />
                // add a input tag and pass our placeholder text to the getInputProps function. We also have an onChange eventlistener on the input field
                <input {...getInputProps({
                  placeholder: "Search movies",
                  onChange: this.inputOnChange
                })} />
                // if the input element is open, render the div else render nothing
                {isOpen ? (
                  <div className="downshift-dropdown">
                    {
                      // filter the movies in the state
                      this.state.movies
                        .filter(item => !inputValue || item.title.toLowerCase().includes(inputValue.toLowerCase()))
                        .slice(0, 10) // return just the first ten. Helps improve performance
                        // map the filtered movies and display their title
                        .map((item, index) => (
                          <div
                            className="dropdown-item"
                            {...getItemProps({ key: index, index, item })}
                            style={{
                              backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                              fontWeight: selectedItem === item ? 'bold' : 'normal',
                            }}>
                            {item.title}
                          </div>
                        ))
                    }
                  </div>
                ) : null}
              </div>
            )}
          </Downshift>
        )
      }
    }

In the code above, we have a class component where we’re rendering the <Downshift/> component and passing our props to it. In the input field in the <Downshift/> component, we have an onChange event listener that listens for new input to the field. If there’s an input, we call the fetchMovies method which takes the value of the input field and makes an AJAX request to the movies API using Axios. We set the request response to the component state, then filter through them to return the matching item as done in the previous example.

Import and render the component in the parent App component as we did in the previous example. Visit your browser and try searching for your favorite movie:

Dropdown with Downshift

One other use case for Downshift is powering dropdowns. Dropdown’s API helps us build simple dropdown components. Let’s create a DownshiftThree.js file in the src folder and see how to achieve this. In the DownshiftThree.js file, add the following code:

    // src/DownshiftThree.js

import React, { Component } from 'react'
import Downshift from 'downshift';

export default class DownshiftThree extends Component {
      constructor(props) {
        super(props)
        this.books = [
          { name: 'Harry Potter' },
          { name: 'Net Moves' },
          { name: 'Half of a yellow sun' },
          { name: 'The Da Vinci Code' },
          { name: 'Born a crime' },
        ];

        this.state = {
          // currently selected dropdown item
          selectedBook: ''
        }

        this.onChange = this.onChange.bind(this)
      }

      onChange(selectedBook) {
        this.setState({ selectedBook: selectedBook.name })
      }

      render() {
        return (
          <Downshift onChange={this.onChange} selectedItem={this.state.selectedBook} itemToString={books => (books ? books.name : '')}>
          // pass the downshift props into a callback
            {({ isOpen, getToggleButtonProps, getItemProps, highlightedIndex, selectedItem: dsSelectedItem, getLabelProps }) => (
              <div>
                // add a label tag and pass our label text to the getLabelProps function
                <label style={{ marginTop: '1rem', display: 'block' }} {...getLabelProps()}>Select your favourite book</label> <br />
                // add a button for our dropdown and pass the selected book as its content if there's a selected item
                <button className="dropdown-button" {...getToggleButtonProps()}>
                  {this.state.selectedBook !== '' ? this.state.selectedBook : 'Select a book ...'}
                </button>
                <div style={{ position: 'relative' }}>
                  // if the input element is open, render the div else render nothing
                  {isOpen ? (
                    <div className="downshift-dropdown">
                      {
                        // map through all the books and render them
                        this.books.map((item, index) => (
                          <div
                            className="dropdown-item"
                            {...getItemProps({ key: index, index, item })}
                            style={{
                              backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                              fontWeight: dsSelectedItem === item ? 'bold' : 'normal',
                            }}>
                            {item.name}
                          </div>
                        ))
                      }
                    </div>
                  ) : null}
                </div>
              </div>
            )}
          </Downshift>
        )
      }
    }

In the code above, we have a DownshiftThree class component where we render the <Downshift/> component. In the callback passed to it, we have a button where we pass the getToggleButtonProps function. The button houses a ternary operator where we set the content the button based on whether the selectedBook in the component’s state is set. Next, we call the isOpen prop to see if the dropdown is open or not. If it is open, we map through all the books and render them in the dropdown.

In the onChange method passed to the <Downshift/> component, whenever an item is selected, we set it the state, thereby updating the content of the button. Import and render the component to the parent App component and reload your browser. You should see something like this:

Forms with Downshift

In this example, we’ll be using a Downshift input component as input fields in a form and attempting to submit the form data. In the src directory, let’s create two files: DownshiftInputField.js and DownshiftFour.js.

In the DownshiftInputField.js, we’ll create an input component with Downshift and use it to render some input fields in the DownshiftFour.js file. Let’s create a functional component in our DownshiftInputField.js file:

    // src/DownshiftInputField.js

import React from 'react'
import Downshift from 'downshift';

export default ({ items, onChange, label, placeholder, name }) => {
      return (
        <Downshift onChange={onChange} itemToString={items => (items ? items.name : '')}>
          {({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex, selectedItem, getLabelProps }) => (
            <div>
            // add a label tag and pass our label text to the getLabelProps function
              <label style={{ marginTop: '1rem', display: 'block' }} {...getLabelProps()}>{label}</label> <br />
              // add an input tag and pass our placeholder text to the getInputProps function
              <input name={name} {...getInputProps({ placeholder })} />
              // if the input element is open, render the div else render nothing
              {isOpen ? (
                <div className="downshift-dropdown">
                  {
                    items
                      // filter the items and return those that includes the inputValue
                      .filter(item => !inputValue || item.name.toLowerCase().includes(inputValue.toLowerCase()))
                      // map through the returned items and render them to the page
                      .map((item, index) => (
                        <div
                          className="dropdown-item"
                          {...getItemProps({ key: item.name, index, item })}
                          style={{
                            backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                            fontWeight: selectedItem === item ? 'bold' : 'normal',
                          }}>
                          {item.name}
                        </div>
                      ))
                  }
                </div>
              ) : null}
            </div>
          )}
        </Downshift>
      )
    }

In the code above, our functional component takes in an array of items, an onChange function, a label and placeholder text and finally a name. The component returns a <Downshift/> component which receives all the required props in a callback function. In the callback, we have a label and an input field. As with other examples, we pass the isOpen prop to a ternary operator. If the input field is open, we filter through the array of items to return items that match the inputValue , then we map through the returned items and render them to the DOM.

Now that our input field component is ready, let’s import it into the DownshiftFour.js file:

    // src/DownshiftFour.js

import React, { Component } from 'react'
import DownshiftInputField from './DownshiftInputField';

export default class DownshiftFour extends Component {
      constructor(props) {
        super(props)
        this.state = {
          books: [
            { name: 'Harry Potter' },
            { name: 'Net Moves' },
            { name: 'Half of a yellow sun' },
            { name: 'The Da Vinci Code' },
            { name: 'Born a crime' },
          ],
          movies: [
            { name: 'Harry Potter' },
            { name: '12 Strong' },
            { name: 'Half of a yellow sun' },
            { name: 'Gringo' },
            { name: 'Black Panther' },
          ],
          book: '',
          movie: ''
        }
        this.onSubmit = this.onSubmit.bind(this);
        this.onChange = this.onChange.bind(this);
      }

      onSubmit(event) {
        event.preventDefault();
        alert(`
        Favourite book: ${this.state.book}
        Favourite movie: ${this.state.movie}
        has been submitted
        `)
      }

      onChange(selectedBook, stateAndHelpers) {
        const element = document.querySelector(`#${stateAndHelpers.id}-input`)
        this.setState({ [element.name]: selectedBook.name })
      }

      render() {
        return (
          <form onSubmit={this.onSubmit}>
            <DownshiftInputField
              items={this.state.books}
              onChange={this.onChange}
              label="Select your favourite book"
              name="book"
              placeholder="Search your favourite book" />
            <DownshiftInputField
              items={this.state.movies}
              onChange={this.onChange}
              label="Select your favourite movie"
              name="movie"
              placeholder="Search your favourite movie" />
            <input type="submit" value="Submit" className="dropdown-button" />
          </form>
        )
      }
    }

In our DownshiftFour.js file, we imported our input field component and created a class component. In our component state, we have an array of books and movies and we render our input field component twice in a form: one as an input field for books and another for movies.

Downshift’s onChange function takes in a second parameter called stateAndHelpers which gives us some information about Downshift’s current state. In the onChange method, we find the input field the user is currently interacting with by getting it’s id from the stateAndHelpers argument and querying the DOM for it. Once we have the element, we set it to the component state.

When the user hits the submit button, the onSubmit method, gets the selected book and movie from the state and does whatever we want with it.

Import and render the <DownshiftFour/> component in the parent App component and let’s give it a spin in the browser. You should have something similar to this:

Downshift with Popper.js

In our last example for this article, we’ll be using Popper.js to change the directions of a Downshift popup. Popper is an awesome library that makes positioning tooltips or popups a straightforward task. We already installed the react-popper package while setting up the application, so let’s create a DownshiftFive.js file in the src folder. Let’s add the following code to the new file:

    // src/DownshiftFive.js

import React, { Component } from 'react'
import Downshift from 'downshift';
import { Popper, Manager, Target } from 'react-popper';

export default class DownshiftFive extends Component {
      // define some default props
      static defaultProps = {
        positions: [
          'top',
          'top-start',
          'top-end',
          'bottom-start',
          'bottom',
          'bottom-end',
        ]
      }

      render() {
        return (
          <div className="popper-div">
          // wrap the whole component in Popper's <Manager/> component
            <Manager>
              <Downshift render={({ inputValue, getInputProps, getItemProps, isOpen, selectedItem, highlightedIndex }) => (
                <div>
                // wrap our input element in Popper's <Target/> component
                  <Target>
                    <input {...getInputProps({ placeholder: 'Enter a position' })} />
                  </Target>
                  <div className="downshift-dropdown">
                    {isOpen ? (
                    // pass the selected item to Popper
                      <Popper
                        placement={selectedItem || 'bottom'}
                        style={{ backgroundColor: 'skyblue' }}
                      >
                        {
                        // filter through all the positions and return the ones that include the inputValue
                          this.props.positions
                            .filter(item => !inputValue || item.includes(inputValue.toLowerCase()))
                            // map through all the returned positions and render them
                            .map((item, index) => (
                              <div className="downshift-item popper-item"
                                {...getItemProps({ item })}
                                key={item}
                                style={{
                                  cursor: 'pointer',
                                  backgroundColor: highlightedIndex === index ? '#bed5df' : 'transparent',
                                  fontWeight: selectedItem === item ? 'bold' : 'normal',
                                }}>
                                {item}
                              </div>
                            ))
                        }
                      </Popper>
                    ) : null}
                  </div>
                </div>
              )}></Downshift>
            </Manager>
          </div>
        )
      }
    }

In the code snippet above, we create a DownshiftFive class components with a default position props. These are the positions Popper will use to render our popup. In the render method of the component, we’re returning a <Downshift/> component wrapped in a <Manager/> component:

return (
    <div className="popper-div">
    <Manager>
        <Downshift render={({ inputValue, getInputProps, getItemProps, isOpen, selectedItem, highlightedIndex }) => (
        {/* all of our remaining code goes here */}
        )}>
        </DownShift>
      </Manager>
      </div>
    )

The Manager component is a wrapper exposed by Popper that needs to surround all other react-popper components on the page to make them communicate with each other. If you look closely, you’ll see we’re passing a render prop to the <Downshift/> component. This is another way to pass our props to the <Downshift/> component. Basically, we moved our callback and passed it to the render prop. In the callback passed to the render prop, we wrap our input field in a <Target/> component. This informs Popper that this is the input field around which the popup should be rendered.

Next, we check if our input is open and render a <Popper/> component and pass our selectedItem to it’s placement prop. This helps Popper reposition the popup whenever a new position is selected. Lastly, as with other examples, we filter all the default prop positions, return the positions that include the inputValue, map through them and render them on the page.

Finally, import and render the <DownshiftFive/> component in the parent App component and check it out in the browser. You should have something similar to this:

Conclusion

That’s it! In this post, we’ve explored common React dropdown use cases solved with Downshift. I hope you’ve learnt a thing or two.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace