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.
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;
};
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 */
`;
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>
);
}
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:
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;
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
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>
);
}
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
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.
Top comments (2)
Very helpful, thanks @danilo95
Great work. Thanks