Fetch Data In React As User Types Or Clicks
Open this example in our interactive code editor to experiment with it.
Try the EditorLet’s dive into a bit more complex examples of real-world scenarios. Fetching data from an API as a user types or clicks is a common pattern in modern web applications. We’ll build two practical examples: a search-as-you-type feature and a fetch-on-click pattern.
If you’re new to React, you might want to check out some fundamentals first. This post assumes you’re familiar with React hooks like useState and useEffect.
Fetch data as we type
The search-as-you-type pattern is everywhere - from Google’s search bar to GitHub’s repository search. Let’s build one step by step.
Prepare data fetching
First, let’s set up the basic data fetching logic. We’ll use the Pokemon API as our data source since it’s free and doesn’t require authentication:
import React, { useState, useEffect } from "react";
const App = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchPokemon = async (name) => {
try {
setLoading(true);
setError(null);
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`
);
if (!response.ok) {
throw new Error("Pokemon not found");
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
setData(null);
} finally {
setLoading(false);
}
};
return (
<div>
<h1>Pokemon Search</h1>
{loading && <p>Loading...</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
{data && (
<div>
<h2>{data.name}</h2>
<img src={data.sprites.front_default} alt={data.name} />
</div>
)}
</div>
);
};
export default App;
This gives us the foundation - state management for data, loading, and error states, plus an async function to fetch Pokemon data.
Adding search field
Now let’s add a search input so the user can type a Pokemon name:
import React, { useState, useEffect } from "react";
const App = () => {
const [search, setSearch] = useState("");
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchPokemon = async (name) => {
if (!name.trim()) return;
try {
setLoading(true);
setError(null);
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`
);
if (!response.ok) {
throw new Error("Pokemon not found");
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
setData(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPokemon(search);
}, [search]);
return (
<div>
<h1>Pokemon Search</h1>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search for a Pokemon..."
/>
{loading && <p>Loading...</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
{data && (
<div>
<h2>{data.name}</h2>
<img src={data.sprites.front_default} alt={data.name} />
</div>
)}
</div>
);
};
export default App;
We added a search state, an input field, and a useEffect that triggers the fetch whenever the search value changes. But there’s a problem…
Adding missing pieces
The current implementation fires an API call on every single keystroke. If the user types “pikachu”, that’s 7 API calls! We need debouncing - waiting until the user stops typing before making the request.
import React, { useState, useEffect, useRef } from "react";
const useDebouncedEffect = (callback, delay, deps) => {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
const handler = setTimeout(() => {
callback();
}, delay);
return () => {
clearTimeout(handler);
};
}, [...deps, delay]);
};
const App = () => {
const [search, setSearch] = useState("");
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchPokemon = async (name) => {
if (!name.trim()) return;
try {
setLoading(true);
setError(null);
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`
);
if (!response.ok) {
throw new Error("Pokemon not found");
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
setData(null);
} finally {
setLoading(false);
}
};
useDebouncedEffect(
() => {
fetchPokemon(search);
},
500,
[search]
);
return (
<div>
<h1>Pokemon Search</h1>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search for a Pokemon..."
/>
{loading && <p>Loading...</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
{data && (
<div>
<h2>{data.name}</h2>
<img src={data.sprites.front_default} alt={data.name} />
</div>
)}
</div>
);
};
export default App;
The useDebouncedEffect custom hook waits for the specified delay (500ms) after the last change before executing the callback. This means the API is only called once the user stops typing.
Key features of this hook:
- It skips the first render to avoid an unnecessary API call on mount
- It clears the previous timeout on each new keystroke
- It uses a cleanup function to prevent memory leaks
Are we hitting the API too much?
Even with debouncing, there are additional optimizations you can consider:
- Caching - Store previous results so searching for the same term twice doesn’t make a second API call
- Minimum query length - Don’t search until the user has typed at least 3 characters
- AbortController - Cancel pending requests when a new search starts
Here’s the final polished version with these improvements:
import React, { useState, useEffect, useRef, useCallback } from "react";
const useDebouncedEffect = (callback, delay, deps) => {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
const handler = setTimeout(() => {
callback();
}, delay);
return () => {
clearTimeout(handler);
};
}, [...deps, delay]);
};
const App = () => {
const [search, setSearch] = useState("");
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const cache = useRef({});
const abortControllerRef = useRef(null);
const fetchPokemon = useCallback(async (name) => {
if (!name.trim() || name.length < 3) return;
// Check cache first
if (cache.current[name]) {
setData(cache.current[name]);
return;
}
// Cancel previous request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
try {
setLoading(true);
setError(null);
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`,
{ signal: abortControllerRef.current.signal }
);
if (!response.ok) {
throw new Error("Pokemon not found");
}
const result = await response.json();
cache.current[name] = result;
setData(result);
} catch (err) {
if (err.name !== "AbortError") {
setError(err.message);
setData(null);
}
} finally {
setLoading(false);
}
}, []);
useDebouncedEffect(
() => {
fetchPokemon(search);
},
500,
[search]
);
return (
<div>
<h1>Pokemon Search</h1>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search for a Pokemon (min 3 chars)..."
/>
{loading && <p>Loading...</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
{data && (
<div>
<h2>{data.name}</h2>
<img src={data.sprites.front_default} alt={data.name} />
</div>
)}
</div>
);
};
export default App;
Fetch data on click
Sometimes you don’t want to fetch data as the user types. Instead, you want the user to explicitly trigger the fetch by clicking a button. This pattern is simpler to implement:
import React, { useState } from "react";
const App = () => {
const [search, setSearch] = useState("");
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleSearch = async () => {
if (!search.trim()) return;
try {
setLoading(true);
setError(null);
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon/${search.toLowerCase()}`
);
if (!response.ok) {
throw new Error("Pokemon not found");
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
setData(null);
} finally {
setLoading(false);
}
};
return (
<div>
<h1>Pokemon Search</h1>
<div>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Enter a Pokemon name..."
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
/>
<button onClick={handleSearch} disabled={loading}>
{loading ? "Searching..." : "Search"}
</button>
</div>
{error && <p style={{ color: "red" }}>{error}</p>}
{data && (
<div>
<h2>{data.name}</h2>
<img src={data.sprites.front_default} alt={data.name} />
<p>Height: {data.height}</p>
<p>Weight: {data.weight}</p>
<p>
Types: {data.types.map((t) => t.type.name).join(", ")}
</p>
</div>
)}
</div>
);
};
export default App;
The click-based approach is simpler because:
- No debouncing needed
- No
useEffectdependency management - The user has full control over when requests are made
- It works well for expensive API calls or actions with side effects
We also added onKeyPress support so the user can press Enter to search, which improves the user experience.
Summary
We covered two common patterns for fetching data in React:
Search as you type:
- Uses
useEffectto react to input changes - Requires debouncing to avoid excessive API calls
- Can be enhanced with caching and request cancellation
- Best for: autocomplete, live search, filtering
Fetch on click:
- Uses an event handler triggered by user action
- Simpler to implement, no debouncing needed
- Best for: form submissions, explicit searches, expensive operations
Both patterns share the same fundamentals: managing loading, error, and data states. The choice between them depends on your use case and the user experience you want to provide.
Remember to always handle loading and error states - your users will thank you for it.
Ready to level up your coding skills?
Build real projects and grow your portfolio with BigDevSoon.
Start 7-Day Free Trial
Creator of BigDevSoon
Full-stack developer and educator passionate about helping developers build real-world skills through hands-on projects. Creator of BigDevSoon, a vibe coding platform with 21 projects, 100 coding challenges, 40+ practice problems, and Merlin AI.
Related Posts
Fetch Data In React With GraphQL
GraphQL introduces totally new concept for data fetching. One endpoint to rule them all, let's see how we can use it in React.
How To Fetch Data In React From REST API
Let's learn how to fetch data in React using fetch, axios, and react-query.
8 Useful React Components
There is a lot of pre-built, reusable, abstracted, encapsulated, and tested code available to use nowadays.