DEV Community

loading...
Cover image for React autocomplete search input (Debounce)

React autocomplete search input (Debounce)

danilo95 profile image danilo95 ・5 min read

Another highly requested feature, this one consists of:
According to what the user types in an input, we must show suggestions that are in our database.

It is usually used in e commerces, since it allows to improve the user experience and have faster purchases.

for our example we will use as backend the API

This allows according to a parameter to find public api to use.
then what we will do is create an input that according to what the user types, we will show him a list of public api that match the search term.
final project

for this component we require these libraries:
yarn add axios (to make api requests)
yarn add styled-components* (to create css with javascript, btw you can implement the code in a normal sass file)
yarn add lodash.debounce (we’ll go into more detail later)

lets start
first let’s create our requests.js file
This will be in charge of making the request to the api

const url = axios.create({
    baseURL: 'https://api.publicapis.org/',
});

export const getApiSuggestions = (word) => {
    let result = url
        .get(`/entries?title=${word}`)
        .then((response) => {
            return response.data;
        })
        .catch((error) => {
            return error;
        });

    return result;
};
Enter fullscreen mode Exit fullscreen mode

now lets create our searchInput component, first we need some style with a little help of styled components

import styled from 'styled-components';

export const Input = styled.input`
    width: 222px;
    height: 51px;
    padding: 10px;
    background: #f3f3f3;
    box-shadow: inset 0px 4px 4px rgba(0, 0, 0, 0.1);
    border-radius: 5px;
    border: none;
`;

export const Ul = styled.ul`
    display: contents;
`;

export const Li = styled.ul`
    width: 222px;
    font-weight: bold;
    height: 51px;
    padding: 10px;
    background: #f5f0f0;
    display: block;
    border-bottom: 1px solid white;
    &:hover {
        cursor: pointer;
        background-color: rgba(0, 0, 0, 0.14);
    }
`;

export const SuggestContainer = styled.div`
    height: 240px;
    width: 242px;
    overflow: scroll;
    &::-webkit-scrollbar {
        display: none;
    }
    -ms-overflow-style: none; /* IE and Edge */
    scrollbar-width: none; /* Firefox */
`;
Enter fullscreen mode Exit fullscreen mode

now our component

import React, { useState, useCallback } from 'react';

import { Input, Ul, Li, SuggestContainer } from './style';

export default function SearchInput({
    loading,
    options,
    requests,
    placeholder,
}) {
    const [inputValue, setInputValue] = useState('');

    const updateValue = (newValue) => {
        setInputValue(newValue);
        requests(newValue);
    };

    return (
        <div>
            <Input
                value={inputValue}
                onChange={(input) => updateValue(input.target.value)}
                placeholder={placeholder}
            />
            <SuggestContainer>
                <Ul>
                    {loading && <Li>Loading...</Li>}
                    {options?.entries?.length > 0 &&
                        !loading &&
                        options?.entries?.map((value, index) => (
                            <Li key={`${value.API}-${index}`}>{value.API}</Li>
                        ))}
                </Ul>
            </SuggestContainer>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

now let’s understand the parameters:

loading: this state, passes from the parent, this will allow showing a loading message while we make the corresponding request.
options: this is the array of objects that we want to show as suggestions.
requests: this is the request in which we will perform the search, the parent has the function, but it is this component that executes it.

the functions:
updateValue: we basically work with controlled components, this function is in charge of setting the new input value, and sending that value to our requests

the important part of render code:

render code

first, we validate if loading is true, if this is the case, only the loading value is displayed while the requests are finished
our second validation ensures that loading is false, and that our options array contains some value to display otherwise it is ignored.

.? is an optional chaning allows reading the value of a property located within a chain of connected objects without having to expressly validate that each reference in the chain is valid.
In other words, it will avoid that if the entries property does not exist the array is not there or it will map a null object

lets create our app

import React, { useState, useEffect } from 'react';
import { getApiSuggestions } from './requests';
import SearchInput from './searchInput';
import { MainWrapper } from './style';

function App() {
    const [options, setOptions] = useState([]);
    const [loading, setLoading] = useState(false);

    const getSuggestions = async (word) => {
        if (word) {
            setLoading(true);
            let response = await getApiSuggestions(word);
            setOptions(response);
            setLoading(false);
        } else {
            setOptions([]);
        }
    };

    const getApiUrl = (url) => {
        window.open(url, '_blank');
    };

    return (
        <MainWrapper>
            <SearchInput
                loading={loading}
                options={options}
                requests={getSuggestions}
                onClickFunction={getApiUrl}
                placeholder="find a public api"
            />
        </MainWrapper>
    );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

functions:

getSuggestions: this is the function that we will pass to our component, this first validates that there is a value to search (we will not send empty values, it would be a meaningless request)
If it does not exist, we clean the options object to not show suggestions if the search term is empty.
After this, taking advantage of async await, we wait for the request to finish and return a value and we set it in options, which is the state that we will pass to the component.
getApiUrl: we will pass this function to the component, it basically opens a url in a new tab.

with all of the above our component should work as follows

fist version app

it’s working, but you saw the problem?.
for each letter we make a request to the api.
this is harmful imagine 10 thousand users using your project and to complete a search each user ends up making 20,000 requests to the api, it is unsustainable and bad practice.

So how do we solve it? debouncing

what is debouncing?
its a function that returns a function that can be called any number of times (possibly in quick successions) but will only invoke the callback after waiting for x ms from the last call.

lets rebuild our searchInput

import React, { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';

import { Input, Ul, Li, SuggestContainer } from './style';

export default function SearchInput({
    loading,
    options,
    requests,
    onClickFunction,
    placeholder,
}) {
    const [inputValue, setInputValue] = useState('');

    const debouncedSave = useCallback(
        debounce((newValue) => requests(newValue), 1000),
        []
    );

    const updateValue = (newValue) => {
        setInputValue(newValue);
        debouncedSave(newValue);
    };

    return (
        <div>
            <Input
                value={inputValue}
                onChange={(input) => updateValue(input.target.value)}
                placeholder={placeholder}
            />
            <SuggestContainer>
                <Ul>
                    {loading && <Li>Loading...</Li>}
                    {options?.entries?.length > 0 &&
                        !loading &&
                        options?.entries?.map((value, index) => (
                            <Li
                                key={`${value.API}-${index}`}
                                onClick={() => onClickFunction(value.Link)}
                            >
                                {value.API}
                            </Li>
                        ))}
                </Ul>
            </SuggestContainer>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

functions:

debouncedSave:
first usecallback, pass an online callback and an array of dependencies. useCallback will return a memorized version of the callback that only changes if one of the dependencies has changed.
then using debounce from lodash.debounce we tell it that this function will be launched after a certain time.
in this way we allow the request to only be executed after a certain time, allowing the user to write their real search and not throw queries like crazy.

let’s see the change in practice how it works
final project

eureka, now with our debouncing our function only performs the request after a certain time, this way we give the user time to enter a valid search term.

We avoid filling our api with garbage requests, and we have improved the user experience.

things to improve:
This api does not have a limit, the correct thing would be to set the response limit to 3–5 since showing a list of 50 suggestions is not the most optimal. 3–5 options as suggestions would be ideal.

Complete Code

Discussion

pic
Editor guide