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.
params
is 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);
});
};
}
loadMoviesData
acceptssearchKey
parameter now, which will be passed toloadMovies
call.
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(''); // [!code highlight]
// ... 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
searchKey
is declared with initial value of''
. - An
input
is rendered withvalue
andonChange
set 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]); // [!code highlight]
...
}
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,
useDebouncedEffect
hook works almost likeuseEffect
hook, 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 todebounce
of most utility libraries such aslodash
andunderscore
.
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
useDebouncedEffect
hook and use it in yourApp
.