
ReactJS
Enterprise applications require careful state architecture. Strategic use of custom hooks, composition, and clear separation of concerns ensures code is maintainable, testable, and scalable.
Extract state logic into custom hooks. This reuses logic across components and makes state independent of UI implementation.
Custom hooks are functions that use React hooks. They encapsulate state logic, making it composable and testable independently of components.
import { useState, useCallback } from 'react';
// Custom hook: Single responsibility - counter logic
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
const decrement = useCallback(() => {
setCount((prev) => prev - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
}
// Custom hook: Form state
function useForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Partial<T>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target;
const fieldValue = type === 'checkbox' ? (e.target as HTMLInputElement).checked : value;
setValues((prev) => ({ ...prev, [name]: fieldValue }));
setTouched((prev) => ({ ...prev, [name]: true }));
},
[]
);
const setFieldValue = useCallback((name: string, value: any) => {
setValues((prev) => ({ ...prev, [name]: value }));
}, []);
const resetForm = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
handleChange,
setFieldValue,
resetForm,
setErrors,
};
}
// Use custom hooks in components
function CounterComponent() {
const counter = useCounter(10);
return (
<div>
<p>Count: {counter.count}</p>
<button onClick={counter.increment}>+</button>
<button onClick={counter.decrement}>-</button>
<button onClick={counter.reset}>Reset</button>
</div>
);
}
function FormComponent() {
const form = useForm({ email: '', password: '' });
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Submitted:', form.values);
form.resetForm();
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={form.values.email}
onChange={form.handleChange}
onBlur={() => {}}
/>
<input
name="password"
type="password"
value={form.values.password}
onChange={form.handleChange}
/>
<button type="submit">Submit</button>
</form>
);
}<details>
<summary>📚 More Examples</summary>
// Example: Advanced custom hook - async data fetching
interface UseFetchOptions {
onSuccess?: (data: any) => void;
onError?: (error: Error) => void;
cacheTime?: number;
}
function useFetch<T>(url: string, options: UseFetchOptions = {}) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
setData(result);
options.onSuccess?.(result);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
options.onError?.(error);
} finally {
setLoading(false);
}
}, [url, options]);
return { data, loading, error, refetch: fetchData };
}
// Example: Composing multiple custom hooks
function useUser(userId: string) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
return { user, loading, error };
}
function useUserPosts(userId: string) {
const { data: posts, loading, error } = useFetch(`/api/users/${userId}/posts`);
return { posts, loading, error };
}
function UserProfile({ userId }: { userId: string }) {
const { user, loading: userLoading } = useUser(userId);
const { posts, loading: postsLoading } = useUserPosts(userId);
return (
<div>
{userLoading && <p>Loading user...</p>}
{user && <h1>{user.name}</h1>}
{postsLoading && <p>Loading posts...</p>}
{posts && <p>Posts: {posts.length}</p>}
</div>
);
}</details>
`useReducer` is better for complex state with many updates, while `useState` suits simple state. Understand the trade-offs to choose correctly.
useState is simple but can lead to complex update logic. useReducer centralizes logic but requires more boilerplate.
import { useState, useReducer } from 'react';
// SIMPLE STATE: useState is better
function TodoListWithState() {
const [todos, setTodos] = useState<Array<{ id: string; text: string; done: boolean }>>([]);
const addTodo = (text: string) => {
setTodos([...todos, { id: Date.now().toString(), text, done: false }]);
};
const toggleTodo = (id: string) => {
setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
};
return (
<div>
<button onClick={() => addTodo('New task')}>Add</button>
{todos.map((todo) => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</div>
))}
</div>
);
}
// COMPLEX STATE: useReducer is better
interface TodoState {
todos: Array<{ id: string; text: string; done: boolean }>;
filter: 'all' | 'done' | 'pending';
sortBy: 'date' | 'alphabetical';
lastUpdated: Date;
loading: boolean;
error: string | null;
}
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: string }
| { type: 'REMOVE_TODO'; id: string }
| { type: 'SET_FILTER'; filter: 'all' | 'done' | 'pending' }
| { type: 'SET_SORT'; sortBy: 'date' | 'alphabetical' }
| { type: 'START_LOADING' }
| { type: 'SET_ERROR'; error: string }
| { type: 'CLEAR_ERROR' };
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: Date.now().toString(), text: action.text, done: false }],
lastUpdated: new Date(),
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((t) =>
t.id === action.id ? { ...t, done: !t.done } : t
),
lastUpdated: new Date(),
};
case 'SET_FILTER':
return { ...state, filter: action.filter };
case 'START_LOADING':
return { ...state, loading: true, error: null };
case 'SET_ERROR':
return { ...state, error: action.error, loading: false };
case 'CLEAR_ERROR':
return { ...state, error: null };
default:
return state;
}
}
function TodoListWithReducer() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all',
sortBy: 'date',
lastUpdated: new Date(),
loading: false,
error: null,
});
const filteredTodos = state.todos.filter((t) => {
if (state.filter === 'done') return t.done;
if (state.filter === 'pending') return !t.done;
return true;
});
return (
<div>
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
<button onClick={() => dispatch({ type: 'ADD_TODO', text: 'New task' })}>
Add
</button>
<select
value={state.filter}
onChange={(e) =>
dispatch({ type: 'SET_FILTER', filter: e.target.value as any })
}
>
<option value="all">All</option>
<option value="done">Done</option>
<option value="pending">Pending</option>
</select>
{filteredTodos.map((todo) => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
/>
{todo.text}
</div>
))}
</div>
);
}<details>
<summary>📚 More Examples</summary>
// Example: Combining useState and useReducer
function useOptimisticUpdate<T>(initialValue: T) {
const [value, setValue] = useState(initialValue);
const [isPending, setIsPending] = useState(false);
const updateOptimistically = async (newValue: T, serverUpdate: () => Promise<void>) => {
// Optimistic update
setValue(newValue);
setIsPending(true);
try {
await serverUpdate();
} catch (error) {
// Rollback on error
setValue(initialValue);
throw error;
} finally {
setIsPending(false);
}
};
return { value, isPending, updateOptimistically };
}
function ArticleEditor() {
const { value: title, isPending, updateOptimistically } = useOptimisticUpdate(
'Initial Title'
);
const handleSave = (newTitle: string) => {
updateOptimistically(newTitle, async () => {
const response = await fetch('/api/article', {
method: 'PUT',
body: JSON.stringify({ title: newTitle }),
});
if (!response.ok) throw new Error('Save failed');
});
};
return (
<div>
<input value={title} onChange={(e) => handleSave(e.target.value)} />
{isPending && <p>Saving...</p>}
</div>
);
}</details>
Build complex state from simpler custom hooks. This modular approach makes state logic reusable and testable.
Compose state hooks to create higher-level abstractions. Each custom hook handles one concern, then combine them.
import { useState, useCallback } from 'react';
// Atomic hooks for single concerns
function useAsync<T>(asyncFunction: () => Promise<T>, immediate = true) {
const [state, setState] = useState<{
loading: boolean;
data: T | null;
error: Error | null;
}>({ loading: immediate, data: null, error: null });
const execute = useCallback(async () => {
setState({ loading: true, data: null, error: null });
try {
const response = await asyncFunction();
setState({ loading: false, data: response, error: null });
} catch (error) {
setState({
loading: false,
data: null,
error: error instanceof Error ? error : new Error(String(error)),
});
}
}, [asyncFunction]);
if (immediate) {
execute();
}
return { ...state, execute };
}
// Compose hooks
function useUserWithPosts(userId: string) {
const user = useAsync(() => fetch(`/api/users/${userId}`).then((r) => r.json()), true);
const posts = useAsync(
() => fetch(`/api/users/${userId}/posts`).then((r) => r.json()),
true
);
const isLoading = user.loading || posts.loading;
const error = user.error || posts.error;
return {
user: user.data,
posts: posts.data,
isLoading,
error,
refetchUser: user.execute,
refetchPosts: posts.execute,
};
}
function UserProfile({ userId }: { userId: string }) {
const { user, posts, isLoading, error } = useUserWithPosts(userId);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>{user?.name}</h1>
<p>Posts: {posts?.length}</p>
</div>
);
}┌────────────────────┬─────────────────┬──────────────────┐
│ Pattern │ Complexity │ Use Case │
├────────────────────┼─────────────────┼──────────────────┤
│ useState │ Simple │ Single values │
│ useReducer │ Medium-High │ Complex state │
│ Custom Hook │ Medium │ Reusable logic │
│ Composition │ Medium-High │ Combined hooks │
│ Context API │ High │ Global state │
│ State library │ Very High │ Enterprise │
└────────────────────┴─────────────────┴──────────────────┘Ready to practice? Challenges | Next: Form State at Scale
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward