Advanced Web Programming
React Effects, Lifecycle, Refs and Error Boundaries
PGCP-AC
Rendering calculates a user interface. Effects perform the separate job of synchronizing that interface with systems React does not control, such as timers, connections, browser APIs and third-party widgets. Refs retain mutable handles, cleanup restores symmetry and error boundaries contain rendering failures.
1. Synchronizing with external systems
An effect synchronizes a component with something outside rendering, such as a timer, event subscription or network interaction. Dependency values determine when synchronization needs to run again. Omitting a changing dependency can leave an effect using stale values. An effect may return a cleanup function to stop a timer, unsubscribe or cancel obsolete work. Cleanup also runs before a later setup when dependencies change, not only at final removal.
Class lifecycle methods express related stages: componentDidMount runs after mounting, componentDidUpdate follows updates and componentWillUnmount supports cleanup. These are useful for understanding older code, although effects are not a mechanical one-to-one translation of every lifecycle method. A ref provides access to a DOM element after it has been attached, enabling operations such as focus without using document-wide selectors.
2. Failure and component structure
An error boundary handles rendering and lifecycle errors in descendant components and can display a fallback. It does not ordinarily catch exceptions from unrelated asynchronous callbacks or event handlers. Event and request errors should be handled where those operations occur. Place boundaries around meaningful sections so a failed panel does not necessarily replace the whole interface.
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(timer);
}, []);
The timer is established by the effect and released by its cleanup. A clock component should also store a timezone identifier separately from the current instant, allowing the same instant to be formatted for several cities. During development, React may repeat setup and cleanup checks to reveal incorrect effect behavior; resource management must remain correct under repeated setup. Stable keys determine whether a component retains its identity and state or is recreated.
3. Effects and event-specific work
An effect is appropriate for maintaining a subscription while a component is present. A user-triggered purchase belongs in the event path that represents that purchase; putting it in an effect merely because a flag changed can make the intent harder to follow. Network effects should ignore or cancel obsolete results so a slower earlier response does not overwrite a newer selection.
A class error boundary typically stores whether an error occurred, returns fallback content in render and records diagnostic information through its error lifecycle support. It should give the user a meaningful next action, such as retrying the section, without pretending a failed operation succeeded. Refs can hold timer or request handles, but derived display values belong in state or render calculations. Keeping these roles distinct prevents values from changing without a visible update.
4. Render, event or effect?
Use rendering to calculate values from current props and state. Use event handlers for work caused by a particular user action. Use an effect to keep an external system synchronized while a component is present or while selected reactive values apply.
| Requirement | Correct home |
|---|---|
| Calculate a filtered list | Render calculation |
| Submit an order after a click | Event handler |
| Maintain a chat connection for a room | Effect |
| Focus after a deliberate button action | Event handler using a ref |
| Subscribe to browser resize while mounted | Effect with cleanup |
Using an effect for a derived value adds an avoidable render with stale data. Using an effect for a purchase triggered by a state flag obscures which interaction caused the purchase and can repeat it under remounting.
5. Effect setup and cleanup
function OnlineStatus() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
function update() { setOnline(navigator.onLine); }
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return <p>{online ? 'Online' : 'Offline'}</p>;
}
Setup establishes synchronization; cleanup reverses it. Cleanup runs before setup repeats with new dependencies and when the component leaves the tree. The same function references are used for listener removal.
Timers, observers, connections, subscriptions and request controllers all need lifecycle-aware cleanup. Cleanup should be safe even if setup and cleanup are exercised more than expected during development checks.
6. Dependency reasoning
Every reactive value read by an effect belongs in its dependency list. Reactive values include props, state and functions or variables declared inside the component.
function ChatRoom({ roomId, serverUrl }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
}
If roomId changes, cleanup disconnects the old room and setup connects the new one. Removing a dependency to suppress the linter lies about what setup reads and can leave a stale connection.
To change dependencies, change the code: move constants outside the component, move event-specific work into its event or redesign unstable objects and callbacks. Memoization can stabilize an identity when that stability has a real semantic or performance purpose, but it is not a blanket dependency escape.
7. Empty, omitted and populated dependencies
- Omitting the dependency array requests an effect after every relevant commit.
- An empty array states that setup reads no changing reactive values.
- A populated array reruns when one listed value differs according to dependency comparison.
An empty array does not mean “run once under every circumstance.” Development behavior may intentionally perform an extra setup-cleanup cycle and removing and remounting the component creates a new lifecycle. Write correct synchronization rather than depending on a call count.
8. Stale closures and intervals
An effect callback captures the render in which it was created. This interval uses a functional updater so it does not need to read a captured count:
useEffect(() => {
const id = setInterval(() => setCount(value => value + 1), 1000);
return () => clearInterval(id);
}, []);
If the interval instead reads count, then count is a reactive dependency and recreating the interval each change may or may not match the intended design. Separate “what external resource is synchronized” from “how the next state is calculated.”
9. Data fetching and race conditions
Effects can fetch when a rendered selection must remain synchronized with remote data, although frameworks and data libraries may provide stronger caching, server rendering and deduplication.
useEffect(() => {
const controller = new AbortController();
async function load() {
setStatus('loading');
try {
const response = await fetch(`/api/courses/${courseId}`, {
signal: controller.signal
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const course = await response.json();
setCourse(course);
setStatus('ready');
} catch (error) {
if (error.name !== 'AbortError') setStatus('error');
}
}
load();
return () => controller.abort();
}, [courseId]);
Changing courseId aborts the previous request before starting the new synchronization. When an API cannot be canceled, an ignore flag or request generation number can prevent an obsolete result from committing.
10. Refs and imperative handles
useRef returns a stable object whose current value may change without requesting a render.
function SearchBox() {
const inputRef = useRef(null);
return <>
<input ref={inputRef} aria-label="Search" />
<button type="button" onClick={() => inputRef.current?.focus()}>
Focus search
</button>
</>;
}
Refs suit DOM nodes, timer IDs, previous external handles and other values not used to calculate visible output. If the screen must update when a value changes, use state. Avoid reading or writing arbitrary refs during rendering because it undermines predictable calculation.
11. Layout effects
useLayoutEffect runs after DOM changes but before the browser paints, allowing measurement and synchronous visual correction. It can block painting, so ordinary useEffect is preferred when work can happen after paint.
Measuring a tooltip before positioning is a possible layout-effect case. Fetching data, logging or most subscriptions do not require blocking paint. Server-rendered environments also require care because layout is a client concept.
12. Memoization is not an effect substitute
useMemo caches a calculated value and useCallback caches a function identity. They are performance tools, not semantic guarantees for external synchronization.
const visibleCourses = useMemo(
() => courses.filter(course => course.title.includes(query)),
[courses, query]
);
First write a correct render calculation. Add memoization when profiling or an identity-sensitive child/API justifies it. Memoized calculations must remain pure and include their reactive dependencies.
13. Custom Hooks
A custom Hook extracts reusable stateful logic, not shared state itself:
function useOnlineStatus() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
const update = () => setOnline(navigator.onLine);
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return online;
}
Each call owns independent React state and effects. The custom Hook shares implementation. Use a common provider or external store when calls must share one underlying value.
14. Class lifecycle methods
Class components express related phases with lifecycle methods:
componentDidMountruns after initial commit;componentDidUpdateruns after an update and usually compares previous values;componentWillUnmountreleases resources before removal.
An effect models a synchronization process with setup and cleanup rather than mapping mechanically to one lifecycle name. One class component may split related subscription logic across mount, update and unmount; one effect can keep that relationship together.
15. Error boundaries
An error boundary catches errors thrown while rendering descendants and in selected descendant lifecycle processing, records diagnostic information and renders fallback UI. In traditional React code, a class implements boundary lifecycle support:
class PanelBoundary extends React.Component {
state = { failed: false };
static getDerivedStateFromError() { return { failed: true }; }
componentDidCatch(error, info) { reportError(error, info); }
render() {
if (this.state.failed) return <p role="alert">This panel could not be displayed.</p>;
return this.props.children;
}
}
Boundaries do not ordinarily catch errors in event handlers, timers, arbitrary promise callbacks, server rendering or the boundary itself. Handle operational failures where they occur. A fallback should state what failed and offer a meaningful recovery path where possible.
16. Boundary placement and reset
Place boundaries around meaningful regions. One top-level boundary prevents a blank application but may replace everything; boundaries around independent panels allow unaffected areas to remain usable. Too many tiny boundaries produce noisy architecture.
A changed key can remount a boundary and its subtree, resetting their state. Use that deliberately for retry or changed identity, not as a general way to hide state-design problems.
17. Time and clocks
A clock effect can update the current instant on an interval. Formatting several cities requires that instant plus explicit time-zone identifiers. Storing only formatted local strings loses the relationship between instant and zone.
const formatter = new Intl.DateTimeFormat('en-IN', {
timeZone: zone,
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
return <time>{formatter.format(now)}</time>;
The interval is an external synchronization resource and needs cleanup. The formatted text is derived during render and need not be additional state.
18. Practical considerations
- Effects synchronize with external systems; they are not a default place for calculations.
- Cleanup runs before replacement setup as well as on removal.
- Missing dependencies create stale synchronization.
- Empty dependencies claim no changing reactive values are read.
- Development can repeat setup and cleanup to expose defects.
- Ref updates do not trigger renders.
- Use layout effects only when work must precede paint.
- Memoization does not replace correct dependency reasoning.
- Error boundaries do not catch every async or event error.
- Changing a key can reset component and boundary state.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.