React Hooks, State, and Application ArchitectureIntroductionInteractive web applications are distinguished from static pages by their ability to remember information, respond to events, coordinate components, communicate with services, and present different views of the same underlying data. In React, these responsibilities are organized through components, state, props, hooks, and architectural boundaries. A small component may need only local state, such as whether a menu is open. A larger application may also require shared client state, server state, authentication context, cached queries, reducers, and carefully defined data-flow boundaries. The central architectural challenge is not to place all data in one global store. It is to place each kind of state at the narrowest sensible scope, update it predictably, and derive rather than duplicate information whenever possible. This chapter develops a systematic approach to React state architecture. It covers built-in and custom hooks, state lifting, immutable updates, Context, reducers, derived state, and the distinction between local, shared, server, and global state. Learning ObjectivesBy the end of this chapter, you should be able to: - Explain React state, props, rendering, and one-way data flow.
- Select suitable scopes for local, lifted, Context-based, global, and server state.
- Apply
useState, useReducer, useEffect, useMemo, useCallback, and useRef appropriately. - Design custom hooks with clear inputs, outputs, and lifecycle behavior.
- Perform immutable updates on objects, arrays, and nested structures.
- Distinguish source state from derived state and avoid synchronization bugs.
- Use Context and reducers to implement coherent shared state.
- Evaluate architectural trade-offs involving performance, testability, accessibility, and maintainability.
1. React’s State Model1.1 Props, state, and renderingProps are inputs supplied by a parent component. They are read-only from the child’s perspective. State is data owned by a component or by an associated state-management mechanism. A render is a calculation of the user interface from current props and state. Conceptually: UI = render(props, state) This is not a literal React API formula; it is a design model. When state changes, React schedules a new render. The component function runs again, producing a new description of the interface. React then commits the necessary changes to the browser. A state update should therefore be understood as a request for a future render, not as an immediate mutation of the currently executing function’s local variables. For example: function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
setCount(count + 1);
}
return <button onClick={increment}>{count}</button>;
}Both expressions use the same count value from the current render. To apply sequential updates reliably, use a functional updater: function increment() {
setCount(previous => previous + 1);
setCount(previous => previous + 1);
}Each updater receives the latest queued value. Functional updates are particularly important when several updates may be batched or when an update depends on previous state. 1.2 One-way data flowReact applications conventionally move data downward through props and move events upward through callback props. A parent owns state when multiple children need to coordinate around the same source of truth. This is called lifting state up. function TemperatureConverter() {
const [celsius, setCelsius] = useState('');
const fahrenheit = celsius === '' ? '' : (Number(celsius) * 9) / 5 + 32;
return (
<>
<TemperatureInput value={celsius} onChange={setCelsius} />
<output>{fahrenheit === '' ? '' : `${fahrenheit} °F`}</output>
</>
);
}The Celsius value is the source state. Fahrenheit is derived state and should not be stored independently. If both values were stored, every update would need to keep them synchronized, creating opportunities for inconsistency. 2. Choosing the Scope of StateState should be classified before a storage mechanism is selected. A useful classification is shown below. | State category | Typical examples | Suitable first choice |
|---|
| Local UI state | Modal visibility, input draft, selected tab | useState | | Lifted/shared client state | Filters used by sibling components, wizard progress | Common parent or useReducer | | Context state | Theme, locale, authenticated user, dependency configuration | Context, often with a reducer | | Global client state | Cross-cutting cart, complex workflow, offline draft | Dedicated store or carefully designed Context | | Server state | Products, messages, permissions fetched from an API | Fetching/cache library or custom server-state hook | | Derived state | Totals, filtered lists, validation status | Calculate during rendering or memoize when justified |
A practical decision sequence is: - Can the value be calculated from props or existing state? If yes, derive it.
- Is it used by only one component? Keep it local.
- Is it required by siblings? Lift it to their nearest common parent.
- Is it cross-cutting and relatively stable, such as theme or locale? Consider Context.
- Is it complex, event-driven, or shared across distant features? Consider a reducer or store.
- Does it originate on a server and need caching, refetching, or synchronization? Treat it as server state.
Global state is not automatically superior. Wider scope increases the number of components affected by changes and can make dependencies less visible. Local ownership usually improves cohesion and testability. 3. The Rules and Semantics of HooksHooks are functions that allow function components to use React features. The two fundamental rules are: - Call hooks only at the top level of a component or custom hook.
- Call hooks only from React function components or custom hooks.
Hooks must not be called inside loops, conditions, nested functions, or event handlers. React associates hook state with call order. If call order changes between renders, React can no longer reliably match state cells with their declarations. Correct: function Panel({ enabled }) {
const [open, setOpen] = useState(false);
if (!enabled) return null;
return <button onClick={() => setOpen(!open)}>{open ? 'Close' : 'Open'}</button>;
}Incorrect: // Do not do this:
if (enabled) {
const [open, setOpen] = useState(false);
}The eslint-plugin-react-hooks rules are an important development safeguard, but developers must still understand the underlying reasoning. 4. Built-in Hooks4.1 useStateuseState is appropriate for independently meaningful values with relatively simple transitions.
const [email, setEmail] = useState('');
const [isSaving, setIsSaving] = useState(false);For expensive initial computation, pass an initializer function so it is evaluated for initialization rather than as an ordinary value expression on every render: const [settings, setSettings] = useState(() => loadInitialSettings()); State updates replace the state value; they do not merge objects automatically. Therefore, preserve unchanged properties explicitly: setProfile(previous => ({
...previous,
displayName: nextName
}));4.2 useReducerA reducer is a pure function that calculates the next state from the current state and an action: nextState = reducer(currentState, action) It should not perform network requests, mutate its arguments, read the clock, or generate random values. A reducer is valuable when transitions are numerous, related, or easier to describe as domain events. function cartReducer(state, action) {
switch (action.type) {
case 'itemAdded': {
const existing = state.items.find(item => item.id === action.item.id);
const items = existing
? state.items.map(item =>
item.id === action.item.id
? { ...item, quantity: item.quantity + 1 }
: item
)
: [...state.items, { ...action.item, quantity: 1 }];
return { ...state, items };
}
case 'itemRemoved':
return {
...state,
items: state.items.filter(item => item.id !== action.id)
};
default:
throw new Error(`Unknown action: ${action.type}`);
}
}Reducers make state transitions explicit and testable. Actions should express meaningful events (itemAdded, checkoutSubmitted) rather than implementation details (setItems), because domain-oriented actions document intent. 4.3 useEffectAn effect synchronizes React with an external system: a browser API, timer, subscription, network request, or imperative library. It is not a general-purpose place for calculations that can occur during rendering. useEffect(() => {
document.title = `${unreadCount} unread messages`;
}, [unreadCount]);The dependency array states which reactive values the effect reads. Cleanup is required when the external system must be disconnected: useEffect(() => {
const connection = connectToRoom(roomId);
connection.open();
return () => connection.close();
}, [roomId]);An effect may run after the component has rendered, and development Strict Mode may intentionally perform additional setup and cleanup checks. Effects must therefore be idempotent in design and must not assume that setup occurs exactly once during development. For asynchronous work, prevent stale results from updating state after the relevant input changes: useEffect(() => {
const controller = new AbortController();
async function load() {
setStatus('loading');
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
});
const data = await response.json();
setUser(data);
setStatus('success');
} catch (error) {
if (error.name !== 'AbortError') setStatus('error');
}
}
load();
return () => controller.abort();
}, [userId]);For production applications, a server-state library may provide caching, deduplication, retries, invalidation, and race handling more completely than a hand-written effect. 4.4 useMemo and useCallbackuseMemo caches a calculated value; useCallback caches a function identity. They are optimization tools, not correctness mechanisms.
const visibleProducts = useMemo(
() => products.filter(product => product.name.includes(search)),
[products, search]
);
const handleSelect = useCallback(
id => setSelectedId(id),
[]
); Memoization is useful when a calculation is genuinely expensive, or when stable identity enables a memoized child or prevents an avoidable effect. It also adds dependency-management complexity and memory overhead. Do not use it reflexively for every value or callback. 4.5 useRefA ref stores a mutable value that persists across renders without causing a render when changed. It is suitable for DOM nodes, timer identifiers, previous values, and imperative handles—not for data that must appear in the UI. const inputRef = useRef(null);
function focusInput() {
inputRef.current?.focus();
}A ref is not a hidden alternative to state. If changing a value should update the interface, state is the appropriate mechanism. 5. Custom HooksA custom hook extracts reusable stateful behavior. Its name begins with use, and it may call other hooks. A custom hook shares logic, not state: each component calling it receives its own independent hook state unless the hook uses a shared mechanism such as Context or an external store. function useOnlineStatus() {
const [online, setOnline] = useState(() => navigator.onLine);
useEffect(() => {
const handleOnline = () => setOnline(true);
const handleOffline = () => setOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return online;
}A well-designed custom hook has a focused responsibility, a small and explicit API, correct cleanup, and behavior that can be tested independently. It should not conceal surprising global side effects. Return objects when named values improve readability, and document whether returned functions are stable identities. 6. Immutable UpdatesReact state should be treated as immutable. Immutability means creating new objects or arrays for changed paths rather than modifying existing state in place. React and memoization commonly use reference identity to detect changes. Incorrect: items.push(newItem);
setItems(items); Correct: setItems(previous => [...previous, newItem]); Common immutable operations include: | Intent | Immutable pattern |
|---|
| Add array item | [...items, item] | | Remove item | items.filter(item => item.id !== id) | | Replace item | items.map(item => item.id === id ? replacement : item) | | Update object | { ...object, property: value } | | Update nested object | { ...state, user: { ...state.user, name } } |
The spread operator performs a shallow copy. It does not deep-copy nested objects. Unchanged nested references may safely be shared when they are not mutated. For complex nested updates, a reducer or an immutable-update utility can improve clarity, but the conceptual requirement remains the same: do not mutate state that React or another component may still reference. Keys in lists also affect identity. Use stable domain identifiers rather than array indexes when items can be inserted, deleted, or reordered. Incorrect keys can cause component state to appear attached to the wrong item. 7. Context and Reducer ArchitectureContext transmits a value through the component tree without manually passing props through every intermediate component. It is appropriate for values that many descendants need, such as theme, locale, authenticated-user information, or a dispatch function. const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, dispatch] = useReducer(authReducer, null);
const value = useMemo(() => ({ user, dispatch }), [user]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const value = useContext(AuthContext);
if (!value) throw new Error('useAuth must be used within AuthProvider');
return value;
}Context is a dependency-injection mechanism, not automatically a complete global state solution. Every consumer of a context may re-render when its provider value changes. Mitigations include splitting contexts by concern, memoizing provider values where useful, keeping provider state appropriately scoped, and using selector-capable external stores when highly granular subscriptions are required. A common architecture separates state and dispatch contexts: const CartStateContext = createContext(null);
const CartDispatchContext = createContext(null); Components that only dispatch actions need not subscribe to changing cart data. This separation improves dependency clarity, although actual performance should be measured rather than assumed. 8. Derived State and Effects: Avoiding Synchronization BugsDerived state is any value that can be calculated from existing props or state. Examples include a filtered list, subtotal, validation result, or whether a form is complete. Prefer: const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
); Avoid: const [subtotal, setSubtotal] = useState(0);
useEffect(() => {
setSubtotal(items.reduce((sum, item) => sum + item.price * item.quantity, 0));
}, [items]);The second version introduces an unnecessary render and a temporary possibility that subtotal does not match items. It may be justified only when the value represents an independently editable or externally synchronized concept. Use useMemo only when calculation cost or identity stability justifies it: const subtotal = useMemo(
() => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
[items]
); A useful distinction is: - Source state: information entered, received, or selected.
- Derived value: deterministic calculation from source state.
- Effect state: information about synchronization, such as loading or connection status.
Keeping these categories separate reduces contradictory states. 9. Server State Versus Client StateServer state has characteristics that local UI state does not: it is remote, shared with other users or processes, potentially stale, asynchronous, cacheable, and subject to authorization. A component may need loading, error, freshness, pagination, cancellation, invalidation, and optimistic updates. A simple request hook might expose: { data, status, error, refetch }However, a mature application generally benefits from a dedicated server-state solution that handles cache keys, stale data, retries, request deduplication, and mutation invalidation. The exact library is an architectural choice; the conceptual separation is the important principle. Client state describes interaction or presentation: a dialog’s visibility, an unsaved form draft, a selected row, or a temporary filter. Do not copy server data into multiple unrelated client stores without a clear synchronization policy. Duplicate caches create conflicting sources of truth. 10. Detailed Applied Case: Campus Equipment BookingThe following is an illustrative case study. BackgroundA university media centre provides cameras, microphones, and lighting equipment. Students search available equipment, select dates, add items to a booking basket, and submit a request. Staff members see pending requests and change their status. ProblemThe initial prototype stores fetched equipment, filters, basket contents, modal visibility, authentication, and request status in one large Context. Every filter keystroke causes the entire application tree to re-render. The basket total is stored separately from basket items, and occasional inconsistencies appear after quantity changes. Failed requests can also overwrite newer successful results. AnalysisThe state has different ownership and lifecycles: - Search text and date selection are local to the booking page.
- Filtered equipment is derived from fetched equipment and filter values.
- Equipment availability is server state.
- Basket contents are shared within the booking workflow.
- Authentication is cross-cutting context state.
- Submission status belongs to the booking operation.
Putting all values in one Context violates scope and cohesion. Storing the basket total duplicates information. A request without cancellation permits stale responses. Approach and solutionThe page keeps filter controls in local state. A server-state hook loads equipment using a key containing the selected dates and location. The visible list is derived with a memoized calculation only after profiling shows the filtering is expensive. A bookingReducer owns basket transitions: const initialBooking = { items: [], submission: 'idle' };
function bookingReducer(state, action) {
switch (action.type) {
case 'added':
return {
...state,
items: state.items.some(item => item.id === action.item.id)
? state.items.map(item =>
item.id === action.item.id
? { ...item, quantity: item.quantity + 1 }
: item
)
: [...state.items, { ...action.item, quantity: 1 }]
};
case 'quantityChanged':
return {
...state,
items: state.items.map(item =>
item.id === action.id
? { ...item, quantity: Math.max(1, action.quantity) }
: item
)
};
case 'submissionStarted':
return { ...state, submission: 'pending' };
case 'submissionSucceeded':
return { items: [], submission: 'succeeded' };
case 'submissionFailed':
return { ...state, submission: 'failed' };
default:
throw new Error(`Unknown booking action: ${action.type}`);
}
}The basket total is calculated during rendering: const total = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
); The reducer and dispatch function are provided only to the booking subtree. Authentication remains in an AuthProvider, while equipment data remains under server-state management. Submission logic uses an abort or request-identity strategy so an old response cannot replace a newer result. OutcomeThe revised design reduces unnecessary subscriptions, eliminates the duplicated total, makes transitions unit-testable, and makes ownership visible. It also supports future requirements such as staff-only actions without forcing every equipment-list component to understand booking mutations. Learning points- State classification should precede tool selection.
- A reducer is valuable for a domain with multiple related transitions.
- Derived totals should not be stored independently.
- Context boundaries should follow stable cross-cutting concerns.
- Server-state synchronization deserves specialized lifecycle handling.
11. Performance, Testing, and Professional PracticePerformanceThe first performance strategy is architectural: keep state near the components that use it. Then measure with the React Profiler and browser tools. Common improvements include splitting large components, using stable keys, virtualizing very large lists, avoiding unnecessary provider changes, and memoizing only demonstrated bottlenecks. A memoized component is not guaranteed to avoid all work. If its props are newly created objects or functions on every render, shallow comparison may still detect changes. Conversely, excessive memoization can make code harder to understand. TestingReducers are especially easy to unit test because they are pure: expect(cartReducer(initial, { type: 'itemRemoved', id: 'x' }))
.toEqual(initial);Component tests should focus on observable behavior: a user enters a value, submits a form, or sees an error. Custom hooks can be tested through a component harness or an appropriate hooks-testing approach. Effects should be tested for cleanup, cancellation, and behavior when dependencies change. Accessibility and securityState architecture does not replace accessibility. Modal state must manage focus, keyboard dismissal, focus return, and appropriate ARIA semantics. Loading and error state should be communicated to assistive technologies where appropriate. Authentication context should not be treated as proof of authorization; the server must enforce permissions. Avoid placing secrets, tokens, or sensitive personal data in unnecessarily broad client contexts, browser storage, or diagnostic logs. Concurrent rendering and modern ReactModern React may start, pause, discard, or repeat rendering work. Components and reducers should therefore remain pure: rendering must not perform irreversible side effects. Effects belong in effect phases, and external stores should integrate through supported subscription mechanisms. Features such as transitions can distinguish urgent input updates from non-urgent visual work, but they do not remove the need for correct state ownership or immutable updates. Challenges and LimitationsNo state pattern solves every problem. Local state can become difficult to coordinate when a workflow spans distant routes. Context reduces prop drilling but can create broad subscriptions and hidden dependencies. Reducers improve predictability but add ceremony for trivial values. External stores may provide selectors and sophisticated subscriptions but introduce another abstraction and operational dependency. Server-state libraries simplify caching but require teams to understand invalidation and freshness semantics. The appropriate design depends on application size, update frequency, team expertise, testing requirements, accessibility needs, and the reliability of the underlying API. Architecture should evolve from measured requirements rather than from the popularity of a particular library. Best-Practice Checklist- Keep state at the narrowest scope that satisfies its consumers.
- Derive deterministic values instead of duplicating them in state.
- Use functional updates when the next value depends on the previous value.
- Treat objects and arrays as immutable state.
- Use reducers for related, complex, or domain-oriented transitions.
- Use effects only for synchronization with external systems.
- Cancel or ignore stale asynchronous work.
- Design Context boundaries around coherent concerns.
- Use refs for persistent mutable values that do not drive rendering.
- Profile before adding memoization.
- Test reducers, custom hooks, cleanup, error paths, and observable behavior.
- Keep authorization on the server and minimize exposure of sensitive data.
Chapter SummaryReact state architecture is principally an exercise in ownership, identity, and synchronization. Props provide read-only inputs, state represents information that can change, and rendering derives the user interface from those inputs. State should normally be kept local, lifted to a common parent when siblings coordinate, placed in Context when it is cross-cutting, or managed by a reducer or external store when transitions and subscriptions become complex. useState handles straightforward state, while useReducer expresses structured domain transitions. useEffect synchronizes with external systems and must include correct dependencies and cleanup. useMemo, useCallback, and useRef solve specific identity, performance, and imperative-reference problems; they should not be used as substitutes for sound ownership. Custom hooks package reusable behavior but do not automatically share state.
Immutable updates preserve predictable reference identity. Derived values should generally be calculated rather than stored. Server state requires special attention to caching, freshness, cancellation, and invalidation. Finally, professional React development combines architecture with profiling, testing, accessibility, security, and awareness of concurrent rendering behavior. Key TermsHook: A function that enables a React component or custom hook to use React features. State: Changeable data that can cause a component to render again. Props: Read-only inputs passed from a parent component to a child. One-way data flow: The pattern in which data moves downward and events communicate upward. State lifting: Moving state to the nearest common parent of components that share it. Local state: State used by a limited component or feature. Context: React’s mechanism for passing values through a component tree without prop drilling. Reducer: A pure function that calculates next state from current state and an action. Action: An object describing an event or intended state transition. Immutable update: An update that creates new changed references without mutating existing state. Derived state: A value calculated from props or other state. Effect: Code that synchronizes React with an external system. Custom hook: A reusable function whose name begins with use and may call hooks. Ref: A persistent mutable container that does not cause rendering when changed. Memoization: Caching a value or function identity to avoid selected repeated work. Server state: Remote data with concerns such as loading, caching, freshness, and invalidation. Global state: State available across broad or unrelated parts of an application. Functional updater: A state update expressed as a function of the previous state. Stale response: An asynchronous result that no longer corresponds to the latest request or input. Provider: A component that supplies a Context value to descendant consumers. Review QuestionsShort Answer Questions- Why should a functional updater be used when a state update depends on the previous value?
- What is the difference between local state and server state?
- State the two fundamental Rules of Hooks.
- Why is a derived subtotal usually not stored as separate state?
- What kind of values are appropriate for
useRef?
Long Answer Questions- Explain how state lifting supports coordination between sibling components. Include an example.
- Compare
useState and useReducer, including situations in which each is appropriate. - Explain Context’s benefits and limitations as a state-management mechanism.
- Describe immutable updates for arrays, objects, and nested objects, and explain why mutation is problematic.
- Explain how effects, cleanup, and cancellation contribute to reliable asynchronous React applications.
Analytical / Application Questions- A dashboard stores fetched orders, filtered orders, total sales, loading status, selected tab, and theme in one Context. Analyze the design and propose more appropriate ownership for each value.
- A search component occasionally displays results for an earlier query after the user has typed a newer query. Identify the likely cause and design a correction.
- Design the state architecture for a multi-step university application form used by several distant components. Justify which values should be local, lifted, reducer-managed, Context-provided, derived, or server-managed.
Multiple Choice Questions with AnswersQuestion 1Which state-management choice is generally most appropriate for a dialog’s open or closed status used by one component? A. Local useState B. A global external store C. A server-state cache D. A database trigger Correct Answer: A. Local useState Question 2What does a functional state updater provide? A. A guarantee that rendering happens synchronously B. Access to the latest queued previous state value C. Automatic persistence to a server D. A way to bypass immutable updates Correct Answer: B. Access to the latest queued previous state value Question 3Which operation performs an immutable removal from an array? A. items.pop() B. delete items[index] C. items.filter(item => item.id !== id) D. items.splice(index, 1) Correct Answer: C. items.filter(item => item.id !== id) Question 4What is the primary purpose of a React effect? A. To calculate ordinary derived values B. To synchronize with an external system C. To replace all reducer logic D. To mutate props safely Correct Answer: B. To synchronize with an external system Question 5Which statement about custom hooks is correct? A. They automatically share state among all callers B. They may be called inside arbitrary event handlers C. They extract reusable hook-based behavior D. They can only contain useState Correct Answer: C. They extract reusable hook-based behavior Question 6Why might a reducer be preferred over several independent useState calls? A. Reducers make network requests automatically B. Reducers express related transitions and domain events coherently C. Reducers eliminate the need for rendering D. Reducers always improve runtime performance Correct Answer: B. Reducers express related transitions and domain events coherently Question 7Which value is most naturally treated as derived state? A. A user’s typed draft text B. Whether a modal is open C. A filtered list calculated from products and a search term D. The result returned by an external API Correct Answer: C. A filtered list calculated from products and a search term Question 8What is a suitable use of useRef? A. Storing a value whose changes must update the UI B. Holding a DOM node reference C. Replacing authenticated server authorization D. Triggering a reducer transition automatically Correct Answer: B. Holding a DOM node reference Question 9What is a major architectural concern with one very large Context provider? A. It prevents descendants from rendering B. It can cause broad re-rendering and hide dependencies C. It makes all values immutable automatically D. It disallows custom hooks Correct Answer: B. It can cause broad re-rendering and hide dependencies Question 10Which characteristic most clearly distinguishes server state from ordinary local UI state? A. It is always a number B. It requires concerns such as freshness, caching, and invalidation C. It cannot be displayed in a component D. It never changes after loading Correct Answer: B. It requires concerns such as freshness, caching, and invalidation
|