DEV Community

loading...
Cover image for How to build a search bar in React

How to build a search bar in React

emma profile image Emma Goto πŸ™ Originally published at emgoto.com on ・6 min read

A search bar is a great way to make content on your website discoverable. In this tutorial, we’ll be building an accessible search bar component using React. We’ll also be adding a couple of unit tests with React Testing Library.

Here's our final product:

The source code for this tutorial is available at react-search-bar.

Render your search bar component in the app

To get started, create a new file for your search component. I've called mine search.js:

// src/search.js
const Search = () => {
    return <div>Hello world!</div>
}

export default Search;
Enter fullscreen mode Exit fullscreen mode

Then, render this component from inside of your main app file:

// src/App.js
import Search from './search';

const App = () => {
    return (
        <Search />
    );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Where did import React from 'react' go?

You may notice that we no longer have an import statement at the top of every file. From the release of React 17, this is no longer necessary. (Yay!) If you happen to be on an older version, you may still need this import statement.

Add your HTML elements

Our search bar component will contain a couple of HTML elements. Add a label, input and button, and then wrap it all in a form element:

// src/search.js
const SearchBar = () => (
    <form action="/" method="get">
        <label htmlFor="header-search">
            <span className="visually-hidden">Search blog posts</span>
        </label>
        <input
            type="text"
            id="header-search"
            placeholder="Search blog posts"
            name="s" 
        />
        <button type="submit">Search</button>
    </form>
);

export default SearchBar;
Enter fullscreen mode Exit fullscreen mode

This will render like this:

Accessibility and labels

You might be wondering why we are doubling up on the label and placeholder text.

This is because placeholders aren’t accessible. By adding a label, we can tell screen reader users what the input field is for.

We can hide our label using a visually-hidden CSS class:

// src/App.css 
.visually-hidden {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}
Enter fullscreen mode Exit fullscreen mode

This keeps it visible to screen reader users, but invisible to everyone else.

Now we have a functioning search bar! When you search, you will navigate to /?s=<your_query_here>.

Add a list of posts

Now that we can search, we’ll need a list of items to search from. I've created a list of fake posts:

const posts = [
    { id: '1', name: 'This first post is about React' },
    { id: '2', name: 'This next post is about Preact' },
    { id: '3', name: 'We have yet another React post!' },
    { id: '4', name: 'This is the fourth and final post' },
];
Enter fullscreen mode Exit fullscreen mode

Use the map function to loop through and render them:

// src/App.js
const App = () => {
    return (
        <div>
            <Search />
            <ul>
                {posts.map((post) => (
                    <li key={post.id}>{post.name}</li>
                ))}
            </ul>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Filter the list based on your search query

Our search bar will navigate us to a new URL when we perform a search. We can grab this value from the URL:

const { search } = window.location;
const query = new URLSearchParams(search).get('s');
Enter fullscreen mode Exit fullscreen mode

We’ll also need a function that filters out posts depending on the search query. If the list you’re querying over is simple, you can write your own:

const filterPosts = (posts, query) => {
    if (!query) {
        return posts;
    }

    return posts.filter((post) => {
        const postName = post.name.toLowerCase();
        return postName.includes(query);
    });
};
Enter fullscreen mode Exit fullscreen mode

You can also rely on third-party search libraries like js-search to filter posts for you.

Using your search query and filter function, you can render the posts that match your search:

// src/App.js
const App = () => {
    const { search } = window.location;
    const query = new URLSearchParams(search).get('s');
    const filteredPosts = filterPosts(posts, query);

    return (
        <div>
            <Search />
            <ul>
                {filteredPosts.map(post => (
                    <li key={post.key}>{post.name}</li>
                ))}
            </ul>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Now when you type in a query, you will be able to filter your posts!

Adding immediate search or β€œsearch as you type”

Instead of pressing enter to submit your search, you may also want the list to filter as the user begins typing. This immediate response can be more pleasant from a user-experience perspective.

To add this feature, you can store a searchQuery value in your component’s state, and change this value as the user begins typing:

// src/App.js
import { useState } from 'react';

function App() {
    const { search } = window.location;
    const query = new URLSearchParams(search).get('s');
    const [searchQuery, setSearchQuery] = useState(query || '');
    const filteredPosts = filterPosts(posts, searchQuery);

    return (
        <div>
            <Search
                searchQuery={searchQuery}
                setSearchQuery={setSearchQuery}
            />
            <ul>
                {filteredPosts.map(post => (
                    <li key={post.key}>{post.name}</li>
                ))}
            </ul>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

After you pass in the searchQuery and setSearchQuery props, you’ll need to make use of it in your input element:

// src/search.js
const SearchBar = ({ searchQuery, setSearchQuery }) => (
    <form action="/" method="get">
        <label htmlFor="header-search">
            <span className="visually-hidden">Search blog posts</span>
        </label>
        <input
            value={searchQuery}
            onInput={e => setSearchQuery(e.target.value)}
            type="text"
            id="header-search"
            placeholder="Search blog posts"
            name="s"
        />
        <button type="submit">Search</button>
    </form>
);
Enter fullscreen mode Exit fullscreen mode

Now, as soon as you start typing, your posts will begin filtering!

Adding SPA navigation with React Router

Currently your search bar will do a full-page refresh when you press enter.
If you're looking to build a single-page app (SPA), you'll want to use a routing library like React Router. You can install it with the following command:

yarn add react-router-dom
Enter fullscreen mode Exit fullscreen mode

After installing it, wrap your app in the Router component:

// src/App.js
import { BrowserRouter as Router } from "react-router-dom";

const App = () => {
    return <Router>
        { /* ... */ }
    </Router>
}
Enter fullscreen mode Exit fullscreen mode

And then add the following to the top of your search component:

// src/search.js
import { useHistory } from 'react-router-dom';

const SearchBar = ({ searchQuery, setSearchQuery }) => {
    const history = useHistory();
    const onSubmit = e => {
        history.push(`?s=${searchQuery}`)
        e.preventDefault()
    };

    return <form action="/" method="get" autoComplete="off" onSubmit={onSubmit}>
Enter fullscreen mode Exit fullscreen mode

Now when a user presses enter, the app's URL will change without a full-page refresh.

β€œSearch as you type”, SPA navigation and accessibility concerns

Without a full-page refresh, you won't be notifying screen reader users if the items in the list change.
We can send these notifications using ARIA live regions.

After some Googling, there are packages like react-aria-live and react-a11y-announcer that will help you do this.
Unfortunately, it seems like neither of these have been updated in over a year.

Luckily, it is simple to write your own announcer component:

// src/announcer.js
const Announcer = ({ message }) =>
    <div role="region" aria-live="polite" className="visually-hidden">{message}</div>

export default Announcer;
Enter fullscreen mode Exit fullscreen mode

And then render this in your main app component:

// src/App.js
<Announcer message={`List has ${filteredPosts.length} posts`}/>
Enter fullscreen mode Exit fullscreen mode

Whenever the message changes in your Announcer component, screen readers will read out the message.

Now, as you search, screen reader users will receive an update letting them know how many posts are on the page.

This isn't a perfect solution, but it's much better than having your items silently change.

If you are on a Mac and testing its VoiceOver feature, make sure to use Safari! I find that other browsers don't work as well with screen readers.

Testing your component with React Testing Library

To wrap things up, we’ll be testing our component using React Testing Library. This library comes out of the box with create-react-app.

The first test we’ll be adding is an accessibility check using axe. To use it, add the jest-axe package to your repository:

yarn add jest-axe
Enter fullscreen mode Exit fullscreen mode

We can use axe to test that our search component does not have any accessibility violations:

// src/search.test.js
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import Search from '../src/search';

expect.extend(toHaveNoViolations);

test('should not have any accessibility violations', async () => {
    const { container } = render(<Search searchQuery='' />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
});

Enter fullscreen mode Exit fullscreen mode

This is a super easy way to catch simple accessibility issues. For instance, if we removed our label component, our test will fail:

We should also add a test for the functionality of your component. Let’s add one that tests that when you type β€œpreact”, it only shows one post:

// src/App.test.js
test('should render one post when user searches for preact', () => {
    render(<App />);

    let posts = screen.getAllByRole('listitem');
    expect(posts.length).toEqual(4);

    const searchBar = screen.getByRole('textbox');
    userEvent.type(searchBar, 'preact');

    posts = screen.getAllByRole('listitem');
    expect(posts.length).toEqual(1);
});

Enter fullscreen mode Exit fullscreen mode

Conclusion

After reading this tutorial, you will be able to create an accessible search bar component for your React app. With unit tests! You can see the full source code at react-search-bar.

If you know more about how to make search bars accessible, I would love to hear from you. Feel free to reach out to me on Twitter at @emma_goto.

Discussion

pic
Editor guide
Collapse
xyn profile image
Mydrax

Hello Emma! Loved the accessibility topics, I love axe and I was happy you mentioned it :)

Quick nit: Don't pass setters to child components, this allows a child component to freely mutate the state of the parent. Children should only react to changes and then mutate the state of a parent accordingly. That way the data flow is predictable.

Collapse
emma profile image
Emma Goto πŸ™ Author

Hi, thanks for the feedback - do you have suggestions on how you would approach this instead?

Collapse
xyn profile image
Mydrax

First off what you did wasn't wrong, with that in mind read the following from the React docs:

There should be a single β€œsource of truth” for any data that changes in a React application. Usually, the state is first added to the component that needs it for rendering. Then, if other components also need it, you can lift it up to their closest common ancestor. Instead of trying to sync the state between different components, you should rely on the top-down data flow.

Lifting state involves writing more β€œboilerplate” code than two-way binding approaches, but as a benefit, it takes less work to find and isolate bugs. 
Enter fullscreen mode Exit fullscreen mode

So, the idea is to only have the parent have full control over the state, the child should receive the parent's state and control to that state as props. This way if there's a bug, you always know where to look for - the parent. You can grant control to the state via props by wrapping the setter in a function in the parent. Doing so will also allow you to control how the child mutates the parent's state too.

TLDR: Wrapping the setter in a function and passing it as a prop, then calling it in the child makes it look like the parent is changing the state, not the child.

Thread Thread
emma profile image
Emma Goto πŸ™ Author

Makes sense, thanks for the detailed explanation :)

Collapse
faiwer profile image
Stepan Zubashev

Mydrax:

const [searchValue, setSearchValue] = useState('');

// 1
return <SearchInput onChange={setSearchValue}/>

// 2
const onChange = (newValue: string) => {
  setSearchValue(newValue);
};

return <SearchInput onChange={setSearchValue}/>;
Enter fullscreen mode Exit fullscreen mode

Do you mean this? I think it's just another one level of bureaucracy. The only difference is that you cannot do this with the wrapper:

onChange(prevValue => ...)
Enter fullscreen mode Exit fullscreen mode

But if you use TypeScript and will declare prop-type like:

onChange: (newValue: stirng): void
Enter fullscreen mode Exit fullscreen mode

you would not be able to do it anyway

I think it's more than "okay" to pass setters below the tree. And it doesn't violate top-down principle. The parent component in both cases is the only source of truth. And the only way to change the state is to use those tools that parent components provide to its children.

P.S. also if you're interested in strong performance you will wrap you wrapper by another wrapper (useCallback) :)

Collapse
starboysharma profile image
Pankaj Sharma

Hello Emma! πŸ˜€ Thank you for this tutorial. I have also implemented a search 🧐 in react. But I am using a different approach. 🀯 I think my code is a bit shorter. Please share your thoughts. As I am new in react and still I am learning πŸ˜…πŸ˜….

dark-todo.netlify.app/
My Code: github.com/Starboy-Sharma/react-to...

Collapse
fiik346 profile image
Taufik Nurhidayat

I want to try, but i use vue :D

Collapse
emma profile image
Emma Goto πŸ™ Author

I'm sure some of this is probably quite similar to Vue - especially the accessibility bits!

Collapse
vaibhavkhulbe profile image
Vaibhav Khulbe

That's a great breakdown! Will feature this article in my newsletter. 😍

Collapse
emma profile image
Emma Goto πŸ™ Author

Wow that's awesome! I'll make sure to subscribe :)

Collapse
vaibhavkhulbe profile image
Vaibhav Khulbe

Thank you so much!

Collapse
lexswed profile image
Lex Swed

Thank you, Emma! Very good read!

Collapse
indoor_keith profile image
Keith Charles

This is so great. One of the first mini how-to's that goes this far in-depth on the accessibility aspect of components. I love it, thanks so much for sharing!

Collapse
emma profile image
Emma Goto πŸ™ Author

Thanks Keith! I had to do a lot of Googling to try and find out the answers to my accessibility questions - definitely an area of improvement for all of us as developers!

Collapse
andrewbaisden profile image
Andrew Baisden

Nice seems to work well.

Collapse
dmahely profile image
Doaa Mahely

Highly informative post, will be coming back to it. Great job πŸ‘

Collapse
emma profile image
Emma Goto πŸ™ Author

Thanks Doaa, glad you like it!

Collapse
bijiabo profile image
Bijiabo

Thanks for sharing! Happy to see that works on accessibility, so niceπŸ‘πŸ‘πŸ‘

Collapse
marchiartur profile image
aRTUR

Thanks for this post!!