What is it good for?
function getWindowDimensions() { const { innerWidth: width, innerHeight: height } = window; return { width, height }; } export function MyComponent() { const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions()); const handleResize = useCallback(() => { setWindowDimensions(getWindowDimensions()); }, []); useEffect(() => { handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [handleResize]); /* ... */
function getWindowDimensions() { const { innerWidth: width, innerHeight: height } = window; return { width, height }; } export function MyComponent() { const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions()); const handleResize = useCallback(() => { setWindowDimensions(getWindowDimensions()); }, []); useEffect(() => { handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [handleResize]); /* ... */
function getWindowDimensions() { const { innerWidth: width, innerHeight: height } = window; return { width, height }; } export function MyComponent() { const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions()); const handleResize = useCallback(() => { setWindowDimensions(getWindowDimensions()); }, []); useEffect(() => { handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [handleResize]); /* ... */
function getWindowDimensions() { const { innerWidth: width, innerHeight: height } = window; return { width, height }; } export function MyComponent() { const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions()); const handleResize = useCallback(() => { setWindowDimensions(getWindowDimensions()); }, []); useEffect(() => { handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [handleResize]); /* ... */
Add a search input and use useEffect to implement a debounce on the pokemon list as a filter.
Add a search input and use useEffect to implement a debounce on the pokemon list as a filter.
Use setTimeout & clearTimeout to build a delay
const timeoutId = setTimeout( () => console.log('execute with timeout'), 300); clearTimeout(timeoutId);
const timeoutId = setTimeout( () => console.log('execute with timeout'), 300); clearTimeout(timeoutId);
const timeoutId = setTimeout( () => console.log('execute with timeout'), 300); clearTimeout(timeoutId);
const timeoutId = setTimeout( () => console.log('execute with timeout'), 300); clearTimeout(timeoutId);
SearchPanel.tsx
type Props = { searchTerm: string; onSearchChanged: (searchTerm: string) => void; }; function SearchPanel({ searchTerm, onSearchChanged }: Props) { return ( <div > <label htmlFor="searchTerm">Search</label> <input type="text" id="searchTerm" value={searchTerm} placeholder="Pokemon name" onChange={(e) => { onSearchChanged(e.target.value); }} /> </div> ); } export { SearchPanel };
ListPage.tsx
function ListPage() { const [searchTerm, setSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); useEffect(() => { const timeoutId = setTimeout(() => setDebouncedSearchTerm(searchTerm), 300); return () => clearTimeout(timeoutId); }, [searchTerm]); /* ... */ }
function ListPage() { const [searchTerm, setSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); useEffect(() => { const timeoutId = setTimeout(() => setDebouncedSearchTerm(searchTerm), 300); return () => clearTimeout(timeoutId); }, [searchTerm]); /* ... */ }
ListPage.tsx
function ListPage() { /* ... */ return ( <> <SearchPanel searchTerm={searchTerm} onSearchChanged={setSearchTerm} /> {pokemons ? ( pokemons ?.filter((p) => p.name.includes(debouncedSearchTerm)) .map((pokemon) => ( <PokeListEntry key={pokemon.name} name={pokemon.name} /> )) ) : ( <div>LOADING</div> )} </> }
function ListPage() { /* ... */ return ( <> <SearchPanel searchTerm={searchTerm} onSearchChanged={setSearchTerm} /> {pokemons ? ( pokemons ?.filter((p) => p.name.includes(debouncedSearchTerm)) .map((pokemon) => ( <PokeListEntry key={pokemon.name} name={pokemon.name} /> )) ) : ( <div>LOADING</div> )} </> }
We can reuse a concrete Hook logic by extracting it into a custom Hook .
function MyComponent() { const result = /* logic that uses Hooks */ return <p>{result}</p>; }
The logic to be extracted can use a single or multiple Hooks.
We simply move the logic into its own function.
function useMyCustomHook() { /* logic that uses Hooks */ return result; } function MyComponent() { const result = useMyCustomHook(); return <p>{result}</p>; }
function useMyCustomHook() { /* logic that uses Hooks */ return result; } function MyComponent() { const result = useMyCustomHook(); return <p>{result}</p>; }
function useMyCustomHook() { /* logic that uses Hooks */ return result; } function MyComponent() { const result = useMyCustomHook(); return <p>{result}</p>; }
In the component we just call this function.
Stick to the Hooks naming convention: use*
usePokemonDetails.ts
/* import ... */ export function usePokemonDetails() { const { pokemonName } = useParams<"pokemonName">(); const uri = `https://pokeapi.co/api/v2/pokemon/${pokemonName}`; const result = useQuery(["pokemon", "detail", pokemonName], () => fetcher<PokemonDetailDto>(uri) ); return result; }
/* import ... */ export function usePokemonDetails() { const { pokemonName } = useParams<"pokemonName">(); const uri = `https://pokeapi.co/api/v2/pokemon/${pokemonName}`; const result = useQuery(["pokemon", "detail", pokemonName], () => fetcher<PokemonDetailDto>(uri) ); return result; }
/* import ... */ export function usePokemonDetails() { const { pokemonName } = useParams<"pokemonName">(); const uri = `https://pokeapi.co/api/v2/pokemon/${pokemonName}`; const result = useQuery(["pokemon", "detail", pokemonName], () => fetcher<PokemonDetailDto>(uri) ); return result; }
DetailPage.tsx
function DetailPage() { const { data, isLoading, isError } = usePokemonDetails(); if (isLoading) return <div>LOADING</div>; if (isError) return <div>ERROR while loading data</div>; return ( <div> <span>{data.name}</span> <img src={data.sprites.front_shiny} alt={data?.name} /> </div> ); }
Stretch Goal: usePokemonDetails.ts
/* import ... */ export function usePokemonDetails(uriBuilder: (pokemonName?: string) => string) { const { pokemonName } = useParams<"pokemonName">(); const uri = uriBuilder(pokemonName); const result = useQuery(["pokemon", "detail", pokemonName], () => fetcher<PokemonDetailDto>(uri) ); return result; }
/* import ... */ export function usePokemonDetails(uriBuilder: (pokemonName?: string) => string) { const { pokemonName } = useParams<"pokemonName">(); const uri = uriBuilder(pokemonName); const result = useQuery(["pokemon", "detail", pokemonName], () => fetcher<PokemonDetailDto>(uri) ); return result; }
Stretch Goal: DetailPage.tsx
/* import ... */ const queryUrlBuilder = (pokemonName?: string) => `https://pokeapi.co/api/v2/pokemon/${pokemonName}`; function DetailPage() { const { data, isLoading, isError } = usePokemonDetails(queryUrlBuilder); if (isLoading) return <div>LOADING</div>; if (isError) return <div>ERROR while loading data</div>; return ( <div> <span>{data.name}</span> <img src={data.sprites.front_shiny} alt={data?.name} /> </div> ); }
/* import ... */ const queryUrlBuilder = (pokemonName?: string) => `https://pokeapi.co/api/v2/pokemon/${pokemonName}`; function DetailPage() { const { data, isLoading, isError } = usePokemonDetails(queryUrlBuilder); if (isLoading) return <div>LOADING</div>; if (isError) return <div>ERROR while loading data</div>; return ( <div> <span>{data.name}</span> <img src={data.sprites.front_shiny} alt={data?.name} /> </div> ); }
We learned…