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 theuseEffect
. This is why we have an asynchronous nestedgetCharacters
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 ouritems
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 calledsetItems
, which we can use to set a new value for theitems
state. - We use the
setItems
function to set theitems
state in thegetCharacters
function after we have mapped the data appropriately from the web API. This call tosetItems
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)