Custom Hooks — Reusable Logic
Level: Beginner
- Extracting logic into custom hooks
- useRef for imperatives and persisting values
- useReducer for complex state machines
- useCallback for memoizing functions
- useMemo for expensive computations
- useLayoutEffect and other advanced hooks
- Building reusable SMS hooks (useFetch, useForm)
- Hook rules and best practices
Why This Matters
Custom Hooks — Reusable Logic is part of building maintainable React applications. You will use it when creating student dashboards, forms, tables, API-connected screens, routing flows, and reusable UI components.
Custom hooks let you extract and reuse component logic. A custom hook is just a JavaScript function using other hooks.
The Problem
Beginners often write React code that works for a small demo but becomes difficult when data, forms, API calls, and reusable components grow. This lesson explains Custom Hooks — Reusable Logic in a way that helps you build predictable UI for real .NET Web API projects.
Creating Your First Custom Hook
Instead of repeating fetch logic:
// ❌ Repeated in every component
function StudentDetail() {
const [student, setStudent] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(`/api/students/1`)
.then(r => r.json())
.then(data => setStudent(data))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, []);
return { student, loading, error };
}
Extract to a custom hook:
// ✓ Reusable
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(data => setData(data))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Now use everywhere
function StudentDetail() {
const { data: student, loading, error } = useFetch('/api/students/1');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return <h1>{student.name}</h1>;
}
function TeacherDetail() {
const { data: teacher, loading } = useFetch('/api/teachers/1');
if (loading) return <p>Loading...</p>;
return <h1>{teacher.name}</h1>;
}
Custom Hook for Form Handling
function useForm(initialValues, onSubmit) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(values);
};
return {
values,
errors,
setErrors,
handleChange,
handleSubmit
};
}
// Usage
function StudentForm() {
const { values, handleChange, handleSubmit } = useForm(
{ name: '', email: '' },
(values) => {
console.log('Submitting:', values);
}
);
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={values.name}
onChange={handleChange}
/>
<input
name="email"
value={values.email}
onChange={handleChange}
/>
<button type="submit">Submit</button>
</form>
);
}
useRef — Accessing DOM Directly
useRef lets you access DOM elements directly.
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</>
);
}
// Get value
function StudentSearch() {
const searchRef = useRef(null);
const handleSearch = () => {
const query = searchRef.current.value;
console.log('Searching for:', query);
};
return (
<>
<input ref={searchRef} type="text" placeholder="Search..." />
<button onClick={handleSearch}>Search</button>
</>
);
}
useReducer — Complex State
For complex state with multiple actions, use useReducer:
function useTodoList(initialTodos) {
const [todos, dispatch] = useReducer(todoReducer, initialTodos);
const addTodo = (text) => {
dispatch({ type: 'ADD', payload: text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE', payload: id });
};
const deleteTodo = (id) => {
dispatch({ type: 'DELETE', payload: id });
};
return { todos, addTodo, toggleTodo, deleteTodo };
}
function todoReducer(todos, action) {
switch (action.type) {
case 'ADD':
return [...todos, { id: Date.now(), text: action.payload, done: false }];
case 'TOGGLE':
return todos.map(t => t.id === action.payload ? { ...t, done: !t.done } : t);
case 'DELETE':
return todos.filter(t => t.id !== action.payload);
default:
return todos;
}
}
// Usage
function TodoApp() {
const { todos, addTodo, toggleTodo, deleteTodo } = useTodoList([]);
return (
<div>
{todos.map(todo => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>
))}
</div>
);
}
useCallback — Memoize Functions
Prevent function recreation on every render:
function useStudentActions() {
const [students, setStudents] = useState([]);
const addStudent = useCallback((student) => {
setStudents(prev => [...prev, student]);
}, []);
const removeStudent = useCallback((id) => {
setStudents(prev => prev.filter(s => s.id !== id));
}, []);
return { students, addStudent, removeStudent };
}
useLocalStorage — Persist State
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
const setValue = (value) => {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
};
return [storedValue, setValue];
}
// Usage
function StudentPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<>
<p>Theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle
</button>
</>
);
}
useAsync — Handle Async Operations
function useAsync(asyncFunction, immediate = true) {
const [status, setStatus] = useState('idle');
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const execute = useCallback(async () => {
setStatus('pending');
try {
const response = await asyncFunction();
setData(response);
setStatus('success');
} catch (err) {
setError(err);
setStatus('error');
}
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { status, data, error, execute };
}
// Usage
function StudentList() {
const { status, data: students } = useAsync(() =>
fetch('/api/students').then(r => r.json())
);
if (status === 'pending') return <p>Loading...</p>;
if (status === 'error') return <p>Error</p>;
return (
<ul>
{students.map(s => <li key={s.id}>{s.name}</li>)}
</ul>
);
}
Custom Hooks Organization
Store in separate files:
// hooks/useFetch.js
export function useFetch(url) {
// ...
}
// hooks/useForm.js
export function useForm(initialValues, onSubmit) {
// ...
}
// hooks/index.js
export { useFetch } from './useFetch';
export { useForm } from './useForm';
// Use
import { useFetch, useForm } from './hooks';
Rules of Hooks
- Only call at top level — Not in loops or conditions
- Only in functions — In components or custom hooks, not regular functions
- Custom hooks start with
use— Convention so React knows they're hooks
Key Takeaways
- Custom hooks extract reusable logic
- useRef for direct DOM access
- useReducer for complex state
- useCallback to memoize functions
- Create library of reusable hooks
- Next: Best practices and patterns
- Calling hooks conditionally — Breaks React's hook system
- Not following rules of hooks — Hooks can't be in regular functions
- Not naming with
use— Confuses other developers - Too much logic in one hook — Break into smaller pieces
Build a library of custom hooks you reuse:
- useFetch
- useForm
- useAsync
- useLocalStorage
- useDebounce
- useThrottle
Use ChatGPT, Claude, or Copilot to go deeper on Custom Hooks. Try these prompts:
"What's the difference between a custom hook and a utility function?""When would you use useRef vs. useState?""Why would you use useReducer instead of useState?""Quiz me on custom hooks"
💡 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
- Custom Hooks — Reusable Logic - The main React concept explained in this lesson.
- Component - A reusable function that returns UI.
- Props/state/effects - Core React ideas used to pass data, remember data, and run side effects.
- Real project usage - How this appears in forms, dashboards, routes, and API-connected pages.
Common Mistakes
- Copying React code without understanding data flow
- Mutating arrays or objects directly instead of creating new values
- Forgetting keys, dependencies, loading states, or error states where needed
- Putting too much logic in one component
- Not testing the screen with realistic School Management System data
Practice Task
Create a small React example using Custom Hooks — Reusable Logic. Keep it focused on one School Management System screen.
Suggested practice:
- Build a small component or page for students, attendance, marks, or fees.
- Pass realistic data into the component.
- Add one success state and one empty/error state where relevant.
- Explain the data flow in your own words.
- Rebuild the same example once without looking at the article.
Quick Revision
| Question | Answer |
|---|---|
| What is the main idea? | Understand and apply Custom Hooks — Reusable Logic in React. |
| Where is it used? | Student dashboards, forms, tables, routes, and API-connected screens. |
| What should beginners focus on? | Clear components, predictable data flow, and small examples. |
| What is the best debugging habit? | Check props, state, render output, and browser console step by step. |
Custom Hooks — Reusable Logic is a React concept used to build clear, reusable, and predictable user interfaces. I would explain the problem it solves, show a small component example, and mention a common mistake beginners should avoid.
It is used in screens like student lists, admission forms, attendance dashboards, marks reports, routing pages, and API-connected admin panels.