I have been working on an SPA with React 18 and The Movie Database (TMDB) API. I am curenly working on the search functionality.
There is a list of search results, as seen below.
For a nice scrollbar on this list, I use React-Perfect-Scrollbar.
I need this list to disapear when I click outside it.
For this purpose, in Searchbox.jsx
, I have:
import PerfectScrollbar from 'react-perfect-scrollbar'
import { ReactComponent as Magnifier } from '../../icons/magnifier.svg';
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import Searchitem from './Searchitem';
function Searchbox() {
const API_URL = 'https://api.themoviedb.org/3';
const [searchInput, setSearchInput] = useState('');
const [results, setResults] = useState([]);
let [isSearch, setIsSearch] = useState(Boolean);
const timeOutInterval = 500;
const searchResultsContainer = useRef(null);
const doMovieSearch = async (e) => {
setSearchInput(e.target.value);
if (e.target.value.length >= 3) {
setIsSearch(true);
const { data: { results } } = await axios.get(`${API_URL}/search/movie`, {
params: {
api_key: process.env.REACT_APP_API_KEY,
query: e.target.value
}
});
setResults(results);
} else {
setIsSearch(false);
}
}
const debounceMovieSearch = (e) => {
setTimeout(() => doMovieSearch(e), timeOutInterval);
}
const hideSearchResults = () => {
setIsSearch(false);
}
const handleOutsideClick = (e) => {
if (!searchResultsContainer.current.contains(e.target)) {
setIsSearch(false);
} else {
setIsSearch(true);
}
}
useEffect(() => {
document.addEventListener('click', handleOutsideClick, true);
}, [])
return (
<form className="search_form w-100 mx-auto mt-2 mt-md-0">
<div className="input-group">
<input className="form-control search-box" type="search" defaultValue={searchInput} onKeyUp={debounceMovieSearch} placeholder="Search movies..." />
<div className="input-group-append">
<button className="btn" type="button">
<Magnifier />
</button>
</div>
</div>
{isSearch ?
<PerfectScrollbar ref={searchResultsContainer} className={"search-results shadow-sm" + (results.length ? ' with-results' : null)}>
{results.length ?
<>
{results.map(movie => (
<Searchitem key={movie.id} movie={movie} hideSearchResults={hideSearchResults} />
))}
</> : <></>
}
{!results.length ?
<>
<p className="no-results">No movies found for this search</p>
</> : <></>
}
</PerfectScrollbar> : <></>
}
</form>
);
}
export default Searchbox;
The handleOutsideClick
method should do that.
The problem
For a reason I have been unable to figure out, clicking outside the search list results in this error:
Cannot read properties of null (reading ‘contains’)
TypeError: Cannot read properties of null (reading ‘contains’)
Questions
- What am I doing wrong?
- What is the most reliable way to fix this issue?
2
Answers
I have solved it by checking if the click event is outside the (parent) form, not the search results list. This code gives a good result:
Your
PerfectScrollbar
component is only displayed whenisSearch
istrue
, so when that state isfalse
, thesearchResultContainer
ref has nocurrent
value (sonull
).You need to, either:
handleClickOutside
listener whenisSearch
isfalse
by using this variable in youruseEffect
dependency array (also, beware that you didn’t set any cleanup function as youruseEffect
return value, which you should).PerfectScrollbar
component, which is always displayed (even if it’s an empty<div>
) and is passed to thesearchResultContainer
ref, so it’s value nevernull
, after the first render that is.I found two open source implementations pf this feature, if you want to look at their source code: