Searching
This section will only be covered if we have additional time during the workshop.
Implementing Search for Movie List
Our current movie list will displays all movies. It would be nice if our user can filter the result by searching with key words. Let’s implement that.
Prerequisite: API Service for Search
First of all, filtering result is usually done via backend as frontend application usually doesn’t have all the data (if it is, it would be very slow to use your application).
Luckily, our current movies API supports search. Open a new browser tab with the following URL and you would see only Aquaman is in the movie list:
https://react-intro-movies.herokuapp.com/movies?q=aqua
The end part of the URL (q=aqua) is how we search the list. Providing different value (e.g. q=bumble) would returns you different results.
Passing Extra Parameter in Ajax Call
Update loadMovies function in api.js to accept a searchKey parameters:
export const loadMovies = (searchKey) =>
getAxios()
.then((axios) =>
axios.default('https://react-intro-movies.herokuapp.com/movies', {
params: { q: searchKey },
})
)
.then((res) => res.data);
- axios accept a second parameter to customize the ajax call.
paramsis an object that will be transformed to query string and append to the end of the ajax call.
Update useMovieData hook in app.js:
function useMovieData() {
// ... existing code
const loadMoviesData = (searchKey) => {
setIsLoading(true);
loadMovies(searchKey).then((movieData) => {
setMovies(movieData);
setIsLoading(false);
});
};
}
loadMoviesDataacceptssearchKeyparameter now, which will be passed toloadMoviescall.
Add an Input to Capture Search Key
Now we need to add an input to our App to capture searchKey.
function App() {
const [moviesShown, toggleShowMovies] = useToggle(false);
const { movies, isLoading, loadMoviesData } = useMovieData();
const { setName, setReleaseDate, setId, values } = useMovieForm();
const [isEdit, setIsEdit] = React.useState(false);
const [searchKey, setSearchKey] = React.useState('');
// ... existing code
return (
<div>
<TitleBar>
<h1>React Movie App</h1>
</TitleBar>
<div className="container">
<div>
<div className="button-container">
<Button onClick={toggleShowMovies}>{moviesShown ? 'Hide' : 'Show'} Movies</Button>
</div>
{moviesShown && (
<BusyContainer isLoading={isLoading}>
{/* highlight-start */}
<div className="field">
<input
value={searchKey}
onChange={(ev) => setSearchKey(ev.target.value)}
className="input"
placeholder="Search for movie..."
/>
</div>
{/* highlight-end */}
<React.Suspense fallback={<span className="spinner" />}>
{movies.map((movie) => (
<Movie
name={movie.name}
releaseDate={movie.releaseDate}
onClick={() => selectMovie(movie)}
key={movie.id}
/>
))}
</React.Suspense>
</BusyContainer>
)}
</div>
</div>
</div>
);
}
- A new state
searchKeyis declared with initial value of''. - An
inputis rendered withvalueandonChangeset accordingly.
Make API call when searchKey change
Currently we can type our search key in the input, but that doesn’t update the movie list. To do that, let’s move the ajax call effects from useMovieData hook into App component:
function useMovieData() {
const [movies, setMovies] = React.useState([]);
const [isLoading, setIsLoading] = React.useState(true);
const loadMoviesData = searchKey => {
setIsLoading(true);
loadMovies(searchKey).then(movieData => {
setMovies(movieData);
setIsLoading(false);
});
};
// React.useEffect(loadMoviesData, []);
return {
movies,
isLoading,
loadMoviesData
};
}
...
function App() {
const [moviesShown, toggleShowMovies] = useToggle(false);
const { movies, isLoading, loadMoviesData } = useMovieData();
const { setName, setReleaseDate, setId, values } = useMovieForm();
const [isEdit, setIsEdit] = React.useState(false);
const [searchKey, setSearchKey] = React.useState('');
React.useEffect(() => loadMoviesData(searchKey), [searchKey]);
...
}
Now our movie list will be updated based on what we type! Done? Nope.
Debounce Effect
Our current code works, but it is unoptimal because we make an AJAX call for every keystroke. We need to “hold on” while user type, and only make the AJAX call after user stop typing.
To do that, let’s create a file use-debounced-effect.js in src/hooks folder:
import * as React from 'react';
export const useDebouncedEffect = (effect, deps, timeout = 500) => {
React.useEffect(() => {
let cleanup;
const timerId = setTimeout(() => {
cleanup = effect();
}, timeout);
return () => {
clearTimeout(timerId);
if (typeof cleanup === 'function') cleanup();
};
}, deps);
};
- In its essence,
useDebouncedEffecthook works almost likeuseEffecthook, with the ability to limits the rate at which the effect can be called. It achieves this by waiting for a buffer time before calling the effect, and if the effect is invoked again before the buffer time finishes, it will cancel the previous call and restart the buffer again. It is conceptually similar todebounceof most utility libraries such aslodashandunderscore.
In our App component, let’s use useDebouncedEffect instead of useEffect hook:
import { useDebouncedEffect } from './hooks/use-debounced-effect';
// ... existing code
function App() {
// ... existing code
useDebouncedEffect(() => loadMoviesData(searchKey), [searchKey]);
// ... existing code
}
Now when you try to search, it will on hold for half a second before start firing the AJAX call. :sunglasses:
Do It: Implement Search Movies Functionalities with Debounce
- Make the changes required to allow user to search movies.
- Verify that the movies is updated when you type to the input field.
- Add
useDebouncedEffecthook and use it in yourApp.
