Skip to main content

useEffect Hook in TypeScript

Level: Beginner to Intermediate

ℹ️ What You'll Learn
  • What useEffect Hook in TypeScript means in React + TypeScript
  • How type safety improves React components
  • How to model School Management System data with interfaces and types
  • Common TypeScript mistakes to avoid
  • How to explain this topic in interviews

Why This Matters

useEffect Hook in TypeScript helps you build React screens with fewer runtime bugs. In real .NET Web API projects, TypeScript makes API DTOs, component props, form state, and shared data contracts easier to understand and safer to change.

TypeScript ensures effect dependencies are correct and cleanup functions are properly typed.

Prerequisite: Read React JS article 07 first.

The Problem

React JavaScript can fail at runtime when props, API responses, or form values have the wrong shape. This lesson shows how useEffect Hook in TypeScript uses TypeScript to catch many of those mistakes while you write code, before the student dashboard reaches users.

Basic useEffect with Types

// Simple effect with typed dependency
useEffect(() => {
document.title = `Student ${studentId}`;
}, [studentId]); // TypeScript checks studentId is number

// Multiple dependencies with types
useEffect(() => {
fetch(`/api/students/${studentId}/class/${classId}`)
.then(r => r.json())
.then(data => setStudent(data));
}, [studentId, classId]); // Both must be correct types

Cleanup Function

interface TimerState {
seconds: number;
}

function StudentTimer() {
const [state, setState] = useState<TimerState>({ seconds: 0 });

useEffect(() => {
const intervalId = setInterval(() => {
setState(prev => ({ seconds: prev.seconds + 1 }));
}, 1000);

// Cleanup function type is automatically inferred as () => void
return () => {
clearInterval(intervalId);
};
}, []); // Empty dependency = run once

return <p>Elapsed: {state.seconds}s</p>;
}

Async Operations in Effects

interface StudentDetail {
id: number;
name: string;
marks: number;
}

function StudentProfile({ studentId }: { studentId: number }) {
const [student, setStudent] = useState<StudentDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
// Track if component is still mounted
let isMounted = true;

const fetchStudent = async (): Promise<void> => {
try {
const response = await fetch(`/api/students/${studentId}`);
if (!response.ok) throw new Error('Failed to fetch');

const data: StudentDetail = await response.json();

// Only update state if component is still mounted
if (isMounted) {
setStudent(data);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err : new Error('Unknown error'));
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};

fetchStudent();

// Cleanup: prevent state updates after unmount
return () => {
isMounted = false;
};
}, [studentId]);

if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <h1>{student?.name}</h1>;
}

Custom Hook with Typed Effect

interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}

function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null
});

useEffect(() => {
let isMounted = true;

fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then((data: T) => {
if (isMounted) {
setState({ data, loading: false, error: null });
}
})
.catch(error => {
if (isMounted) {
setState({ data: null, loading: false, error });
}
});

return () => {
isMounted = false;
};
}, [url]);

return state;
}

// Usage
function StudentList() {
const { data: students, loading, error } = useFetch<Student[]>('/api/students');

if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;

return (
<ul>
{students?.map(s => <li key={s.id}>{s.name}</li>)}
</ul>
);
}

Typed Event Listener

function StudentWindowSize() {
const [width, setWidth] = useState<number>(window.innerWidth);

useEffect(() => {
const handleResize = (event: UIEvent): void => {
if (event.target instanceof Window) {
setWidth(event.target.innerWidth);
}
};

window.addEventListener('resize', handleResize as EventListener);

return () => {
window.removeEventListener('resize', handleResize as EventListener);
};
}, []);

return <p>Width: {width}px</p>;
}

Debounced Search Effect

function StudentSearch() {
const [query, setQuery] = useState<string>('');
const [results, setResults] = useState<Student[]>([]);

useEffect(() => {
const timeoutId = setTimeout(async () => {
if (query.length === 0) {
setResults([]);
return;
}

try {
const response = await fetch(`/api/students/search?q=${query}`);
const data: Student[] = await response.json();
setResults(data);
} catch (error) {
console.error(error);
}
}, 500);

return () => clearTimeout(timeoutId);
}, [query]);

return (
<>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map(s => <li key={s.id}>{s.name}</li>)}
</ul>
</>
);
}

Multiple Effects

function StudentDashboard({ studentId }: { studentId: number }) {
const [student, setStudent] = useState<Student | null>(null);
const [marks, setMarks] = useState<number[]>([]);
const [attendance, setAttendance] = useState<number>(0);

// Effect 1: Fetch student
useEffect(() => {
fetch(`/api/students/${studentId}`)
.then(r => r.json())
.then(data => setStudent(data));
}, [studentId]);

// Effect 2: Fetch marks
useEffect(() => {
fetch(`/api/students/${studentId}/marks`)
.then(r => r.json())
.then(data => setMarks(data));
}, [studentId]);

// Effect 3: Fetch attendance
useEffect(() => {
fetch(`/api/students/${studentId}/attendance`)
.then(r => r.json())
.then(data => setAttendance(data));
}, [studentId]);

// Effect 4: Update document title
useEffect(() => {
if (student) {
document.title = `${student.name} - Dashboard`;
}
}, [student]);

return (
<div>
<h1>{student?.name}</h1>
<p>Marks: {marks.length}</p>
<p>Attendance: {attendance}%</p>
</div>
);
}

Type-Safe Interval

function useInterval(callback: () => void, delay: number | null): void {
useEffect(() => {
if (delay === null) return;

const intervalId = setInterval(callback, delay);
return () => clearInterval(intervalId);
}, [delay, callback]);
}

// Usage
function StudentProgressTracker() {
const [progress, setProgress] = useState(0);

useInterval(() => {
setProgress(p => (p + 1) % 100);
}, 1000);

return <p>Progress: {progress}%</p>;
}

Key Takeaways

  • Type effect callbacks
  • Dependencies are checked by TypeScript
  • Async functions must be inside effect
  • Cleanup function type is inferred
  • Multiple effects for separation of concerns
  • Next: Lists with types
⚠️ Effect Typing Mistakes
  1. Missing dependency types — TypeScript won't catch
  2. Async effect directly — Create async function inside
  3. Not typing cleanup — Return type must be void
  4. Forgetting isMounted — Memory leaks from stale updates
💡 Effect Dependency Rules

Dependencies must be exact. If you reference variables, include them:

// ❌ Missing studentId
useEffect(() => {
fetch(`/api/students/${studentId}`);
}, []); // TypeScript may not catch this!

// ✓ Include all dependencies
useEffect(() => {
fetch(`/api/students/${studentId}`);
}, [studentId]);
🤖Use AI to Learn Faster

Use ChatGPT, Claude, or Copilot to go deeper on Typed Effects. Try these prompts:

  • "Why track isMounted in async effects?"
  • "How do you type event listeners?"
  • "What's the cleanup function type?"
  • "Quiz me on useEffect types"

💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.

Quick Definitions

  • useEffect Hook in TypeScript - The main React + TypeScript concept explained in this lesson.
  • Type/interface - A contract that describes the shape of data.
  • Typed props/state - React data with clear compile-time expectations.
  • API DTO - The request or response shape shared with ASP.NET Core Web API.

Common Mistakes

  • Using any too quickly instead of defining a useful type
  • Typing props loosely and losing the benefit of TypeScript
  • Forgetting that API data can still be missing or invalid at runtime
  • Making types too complex before the component is stable
  • Not sharing clear DTO shapes between frontend and backend teams

Practice Task

Create a small React TypeScript example using useEffect Hook in TypeScript. Keep it connected to a School Management System scenario.

Suggested practice:

  1. Define a clear Student, Teacher, or Attendance type.
  2. Build a small typed component or hook.
  3. Add one valid example and one intentionally wrong example to see TypeScript errors.
  4. Explain the type contract in your own words.
  5. Rebuild the same example once without looking at the article.

Quick Revision

QuestionAnswer
What is the main idea?Use TypeScript to make useEffect Hook in TypeScript safer in React.
Where is it used?Props, state, forms, API responses, context, hooks, and routes.
What should beginners avoid?Overusing any and ignoring runtime API validation.
What is the best debugging habit?Read the TypeScript error, check the data shape, and fix the type contract.
🎯 How would you explain useEffect Hook in TypeScript in an interview?

useEffect Hook in TypeScript is a React + TypeScript concept that improves safety and maintainability. I would explain which values need types, how TypeScript catches mistakes early, and how it helps when consuming ASP.NET Core Web API responses.

🎯 Where is this used in a real React TypeScript project?

It is used in typed props, API response models, form state, route parameters, context values, custom hooks, and reusable UI components.

Next Article

Lists and Keys in TypeScript ->

nexcoding.in