Filter List As You Type with React

A common piece of functionality in many user interfaces is to allow users to filter a list interactively by typing into a text field. In fact, I wrote an article showing how to do this in WPF almost seven years ago.

I’m currently learning React, and I feel this is a good exercise to get the hang of several basic concepts. I am sharing this in case it helps anyone, but my React knowledge is quite limited so I don’t expect anyone to take this as some kind of best practice. I welcome feedback on any possible improvements.

Although this article is quite basic, it covers several topics including controlled components, state manipulation, and keys. I’m not getting into the details of one-way binding and JSX, and just assuming you’re already familiar with them.

Preparing the React Application

The first thing to do is create a new React application. Simply follow the instructions in “Getting Started with React“.

Remove everything from src/App.css, and remove the <header> element from src/App.js as well as the logo import so that you are left with just this:

import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">

    </div>
  );
}

export default App;

If you’re using Visual Studio Code, you can use Ctrl+` (Control backtick) to bring up an integrated terminal. Either way, run npm start from a terminal. You should see an empty page because we just removed everything from it.

Showing a List of Fruit

If we’re going to filter a list, the first thing we need is to show a list. This is easy enough to achieve:

function App() {

  const fruit = ['apple', 'banana', 'orange', 'grapefruit',
    'mango', 'strawberry', 'peach', 'apricot'];

  return (
    <div className="App">
      <ul>
      {fruit.map(f => <li>{f}</li>)}
      </ul>
    </div>
  );
}

We’ve just got an array of strings representing different fruit, and we’re using the JavaScript map() function to render each item within a list.

The list is rendered, but we get a warning about a missing key.

If you save the file, the browser should automatically reload and display the list of fruit as shown above. However, if you open the browser’s developer tools, you’ll notice a warning about some missing key.

When rendering a list of items, React needs each item to be given a unique key to keep track of changes and know when it needs to re-render. This is done by adding a key attribute and binding it to something, as shown below.

      {fruit.map(f => <li key={f}>{f}</li>)}

In our case, we can simply use the name of the fruit itself, but typically you will want to use a unique ID rather than the display string.

State and Controlled Components

The next thing we need is to take input from a text field. We can show a text field by simply adding it to the JSX:

    <div className="App">
      <p>
        Type to filter the list:
        <input id="filter"
          name="filter"
          type="text"
        />
      </p>
      <ul>
      {fruit.map(f => <li key={f}>{f}</li>)}
      </ul>
    </div>

If we want to use the value of the text field (i.e. whatever the user is typing), then we need to link it to the component state. To get to this point, we’ll first introduce the useState() hook as follows:

import React, { useState } from 'react';
import './App.css';

function App() {

  const fruit = ['apple', 'banana', 'orange', 'grapefruit',
    'mango', 'strawberry', 'peach', 'apricot'];

  const [filter, setFilter] = useState('');

  // ...

useState() is simply a function that helps us work with component state, which is where we store any view-related data such as the filter text in our particular eample. Its purpose and functionality might be confusing at first glance, especially because the name is not particularly clear.

Basically, it takes an initial state as a parameter (an empty string in the case of the filter text), and returns an array of two items: the current state of a particular variable, and a function that can assign its value. These roughly correspond to a getter and a setter, except that the getter is the actual value rather than a function (whereas the setter is indeed a function).

We use destructuring to extract these two into separate variables. What’s interesting is that we don’t really need to implement anything more than what you see here: even the setFilter() function is given to us and we don’t need to define it.

Now that we have a way to get and set the filter text within the component’s state, we can update the input field to use this functionality:

        <input id="filter"
          name="filter"
          type="text"
          value={filter}
          onChange={event => setFilter(event.target.value)}
        />

Specifically, we use the current value of filter (from component state) to set the value attribute of the input field, and provide a React event (note the casing which distinguishes it from the onchange DOM event) that updates the component state whenever the value in the input field changes.

In this way, the filter text value in the DOM (input field) is always in sync with the component state, meaning that we can use the value in component state without ever having to touch the DOM directly. This is called a controlled component.

If you’re using the React Developer Tools extension for Chrome, you can see the state value being updated even though we haven’t implemented the list filtering functionality yet:

The component state reflects the value of the input field in the DOM.

Filtering the List

Since it is now easy to retrieve and manipulate the value of the filter text, filtering the list simply becomes a matter of using the JavaScript filter() function when rendering the list:

      <ul>
      {fruit.filter(f => f.includes(filter) || filter === '')
            .map(f => <li key={f}>{f}</li>)}
      </ul>

Each time a user types in the input field, this changes the state of the component, which causes React to re-render it. The list is updated accordingly in real-time:

Note that this filtering is case sensitive, so it won’t work as expected if you type uppercase characters. I didn’t include this level of detail to keep things as concise as possible, but it is easy to adapt this to handle case insensitive filtering.

Complete Code

If you followed the instructions so far, your src/App.js should look like this:

import React, { useState } from 'react';
import './App.css';

function App() {

  const fruit = ['apple', 'banana', 'orange', 'grapefruit',
    'mango', 'strawberry', 'peach', 'apricot'];

  const [filter, setFilter] = useState('');

  return (
    <div className="App">
      <p>
        Type to filter the list:
        <input id="filter"
          name="filter"
          type="text"
          value={filter}
          onChange={event => setFilter(event.target.value)}
        />
      </p>
      <ul>
      {fruit.filter(f => f.includes(filter) || filter === '')
            .map(f => <li key={f}>{f}</li>)}
      </ul>
    </div>
  );
}

export default App;

Summary

You should take away the following from this article:

  • When rendering lists of items, make sure to give each item a unique key.
  • The state of a component contains view-related data.
  • React hooks are simply functions providing ways to access state and life cycle.
  • useState() lets you get and set the value of a variable within state, and also provide an initial value.
  • A controlled component manages input fields (DOM elements) by linking their value to the React component state. This is done by binding an input field’s value to the component state, while at the same time using events to update the value in the component state when the value in the DOM changes.
  • State changes cause a React component to re-render.

Leave a Reply

Your email address will not be published. Required fields are marked *