DEV Community

Cover image for Dropdown data binding with React hooks
Carl
Carl

Posted on • Updated on • Originally published at carlrippon.com

Dropdown data binding with React hooks

Dropdown data binding is always interesting in different UI technologies. We often want to feed the dropdown a list of dynamic data values from a web API. Usually, we want to stop the user from interacting with the dropdown while the items are being loaded. We may wish to select a particular dropdown item after they have loaded as well. So, how do we do all this with React hooks? Let's find out.

Creating the dropdown component

Our dropdown is going to consist of character names from Star Wars. Let's make a start on the React component.

function CharacterDropDown() {
  return (
    <select>
      <option value="Luke Skywalker">
        Luke Skywalker
      </option>
      <option value="C-3PO">C-3PO</option>
      <option value="R2-D2">R2-D2</option>
    </select>
  );
}

This is a functional React component containing 3 hardcoded characters. Although the item labels are the same as the item values in our example, we've explicitly specified them both because often they are different in other scenarios.

A nice and simple start but there is still a lot of work to do!

Using state to render dropdown items

Our dropdown contains hardcoded items at the moment. What if items need to be dynamic and loaded from an external source like a web API? Well, the first thing we need to do to make the item's dynamic is to put the items in state. We can then have the dropdown reference this state when rendering its items:

function CharacterDropDown() {
  const [items] = React.useState([
    {
      label: "Luke Skywalker",
      value: "Luke Skywalker"
    },
    { label: "C-3PO", value: "C-3PO" },
    { label: "R2-D2", value: "R2-D2" }
  ]);
  return (
    <select>
      {items.map(item => (
        <option
          key={item.value}
          value={item.value}
        >
          {item.label}
        </option>
      ))}
    </select>
  );
}

We use the useState hook to create some state with our characters in. The parameter for useState is the initial value of the state. The useState hook returns the current value of the state in the first element of an array - we've destructured this into an items variable.

So, we have an items variable which is an array containing our Star Wars characters. In the return statement, we use the items array map function to iterate through the characters and render the relevant option element. Notice that we set the key attribute on the option element to help React make any future changes to these elements.

We can arguably make the JSX a little cleaner by destructuring the label and value properties from the item that is being mapped over and then referencing them directly:

<select>
  {items.map(({ label, value }) => (
    <option key={value} value={value}>
      {label}
    </option>
  ))}
</select>

Fetching data from a web API

We are going to populate a dropdown with characters from the fantastic Star Wars API. So, instead of putting 3 hardcoded characters in the state, we need to put data from https://swapi.co/api/people into it. We can do this with the useEffect hook:

function CharacterDropDown() {
  const [items, setItems] = React.useState([]);

  React.useEffect(() => {
    async function getCharacters() {
      const response = await fetch("https://swapi.co/api/people");
      const body = await response.json();
      setItems(body.results.map(({ name }) => ({ label: name, value: name })));
    }
    getCharacters();
  }, []);

  return (
    ...
  );
}

Let's examine the useEffect hook:

  • Its first parameter is a function to execute when the side effect runs
  • The second parameter determines when the side effect runs. In our case this is just after the component first renders because we have specified an empty array
  • Our side effect function in the useEffect hook needs to be asynchronous because of the web API call, but this isn't directly allowed in the useEffect. This is why we have an asynchronous nested getCharacters function that is called
  • Inside the getCharacters function we use the native fetch function to make the web API request. We then map the response body to the data structure that our items state expects

Let's turn our attention to the useState hook again:

  • Notice that we now default the items state to an empty array
  • Notice also that we have destructured the 2nd parameter from the useState hook. This is a function called setItems, which we can use to set a new value for the items state.
  • We use the setItems function to set the items state in the getCharacters function after we have mapped the data appropriately from the web API. This call to setItems will cause our component to re-render and show the dropdown items.

Stopping the user interact with the dropdown while items are loading

We probably want to stop the user from interacting with the dropdown while the data is being loaded. We can do this by disabling the dropdown whist the web API request is being made:

function CharacterDropDown() {
  const [loading, setLoading] = React.useState(true);
  const [items, setItems] = React.useState([
    { label: "Loading ...", value: "" }
  ]);
  React.useEffect(() => {
    async function getCharacters() {
      ...
      setItems(body.results.map(({ name }) => ({ label: name, value: name })));
      setLoading(false);
    }
    getCharacters();
  }, []);
  return (
    <select disabled={loading}>
      ...
    </select>
  );
}

We've added a new piece of state called loading to indicate whether items are being loaded. We initialise this to true and set it to false after the items have been fetched from the web API and set in the items state.

We then reference the loading state on the select elements disabled property in the JSX. This will disable the select element while its items are being loaded.

Notice that we've defaulted the items state to an array with a single item containing a "Loading .." label. This is a nice touch that makes it clear to the user what is happening.

Aborting loading items when the component is unmounted

What happens if the user navigates to a different page, and CharacterDropDown is unmounted while the items are still being fetched? React won't be happy when the response is returned, and state is attempted to be set with the setItems and setLoading functions. This is because this state no longer exists. We can resolve this by using an unmounted flag:

React.useEffect(() => {
  let unmounted = false;
  async function getCharacters() {
    const response = await fetch(
      "https://swapi.co/api/people"
    );
    const body = await response.json();
    if (!unmounted) {
      setItems(
        body.results.map(({ name }) => ({
          label: name,
          value: name
        }))
      );
      setLoading(false);
    }
  }
  getCharacters();
  return () => {
    unmounted = true;
  };
}, []);

So, we initialise unmounted to false and check that it is still false before the state is set.

The side effect function in the useEffect hook can return another function that is executed when the component is unmounted. So, we return a function that sets our unmounted to true.

Our dropdown is nice and robust now.

Controlling the dropdown value with state

A common pattern when building a form is to control the field values in state, so, let's now control the dropdown value with state:

function CharacterDropDown() {
  const [loading, setLoading] = React.useState(true);
  const [items, setItems] = React.useState(...);
  const [value, setValue] = React.useState();
  React.useEffect(...);
  return (
    <select
      disabled={loading}
      value={value}
      onChange={e => setValue(e.currentTarget.value)}
    >
      ...
    </select>
  );
}

We've added a new piece of state called value and bound that to the value prop on the select element in the JSX. We also update this state in a change event listener with the onChange prop.

Setting the initial value

We may want to select an initial value of the dropdown. Now that the value is controlled by state, this is a simple matter of setting the default value of the state:

const [value, setValue] = React.useState(
  "R2-D2"
);

Wrap up

  • We use the useEffect hook to load drop down items from a web API. The side effect function needs to contain a nested function that does the web API call
  • We use the useState hook for a loading flag that is set while drop down items are loading which can be used to disable the drop down during this process
  • We use the useState hook to hold drop down items in state. This is set after the data has been fetched from the web API
  • We also use the useState hook to control the selected drop down value in state. We can then set the initial selected value for the drop down by setting the initial value for the state

Originally published at https://www.carlrippon.com/drop-down-data-binding-with-react-hooks on Jan 28, 2020.

Top comments (0)