Enterprise Hook Patterns
Enterprise Hook patterns cover scalable custom Hooks, separation of concerns, server versus UI state, composition, and production-ready architecture guidelines.
Introduction
You have already learned the building blocks of modern React Hooks:
Introduction → Individual Hooks → Rules↓Side Effects → Performance → Debugging↓Best Practices → Real Examples↓Enterprise Patterns
Lessons 1–14 taught you how Hooks work, how to use them correctly, how to debug them, and how to extract reusable Custom Hooks.
This final lesson asks a different question:
How do we organize Hook-based code so a real application stays predictable, reusable, testable, and maintainable as it grows?
A small application may only need:
useStateuseEffectuseRef
inside a few components.
An enterprise application may also need to coordinate:
- authentication
- permissions
- server data
- caching
- forms and multi-step workflows
- browser APIs and subscriptions
- feature flags
- notifications
- analytics
- optimistic updates
- shared application state
- performance-sensitive interfaces
The challenge is no longer knowing what useState or useEffect is.
The challenge is architecture.
What Makes Enterprise React Different?
A small component can own everything:
function Counter() {const [count, setCount] = useState(0);return (<button onClick={() => setCount((value) => value + 1)}>{count}</button>);}
That is still the right design for local UI.
An enterprise feature can involve many responsibilities at once:
Page|+-- Feature UI|+-- Feature state|+-- Server state|+-- Authentication|+-- Permissions|+-- Forms|+-- API communication|+-- Browser integration|+-- Analytics|+-- Error handling
If all of this lives inside one page component, the component becomes difficult to understand, test, and change.
Enterprise Hook patterns exist to create clear boundaries around stateful behavior.
They do not exist to force every app into the same layered architecture.
Architecture Should Match Complexity
A useful pattern in larger applications is:
UI Component|vFeature Hook|vService / Data Layer|vExternal System
This can be a strong default when:
- the feature has substantial behavior
- multiple components need the same workflow
- API communication should be testable
- ownership needs to be obvious to a team
It is not a mandatory rule for every React application.
Architecture should depend on:
- application size
- team size
- domain complexity
- data requirements
- testing requirements
- performance requirements
Avoid overengineering small applications.
A local toggle does not need a feature Hook, a service, and a Context provider.
Separation of Responsibilities
When a feature does grow, give each layer a clear job.
UI Component→ What should the user see?Feature Hook→ How does this feature behave?Service / Data Layer→ How do we talk to the external system?
This builds directly on Custom Hooks from Lesson 8 and the real examples in Lesson 14.
The difference at enterprise scale is intentional ownership:
Who owns this state?Who owns this request?Who owns this subscription?Who owns this business rule?
If the answer is "everyone," the architecture will drift.
Presentation vs Feature Behavior
A common growth path is:
function UserProfilePage() {const [user, setUser] = useState(null);// fetching// updating// validation// permissions// loading state// error handlingreturn (// large amount of JSX);}
As the feature grows, separate behavior from presentation:
function UserProfilePage() {const profile = useUserProfile();return <UserProfileView {...profile} />;}
function UserProfileView({user,status,onUpdate,}) {if (status === "loading") {return <p>Loading...</p>;}return (<section><h1>{user.name}</h1><button onClick={onUpdate}>Update Profile</button></section>);}
The page Hook owns feature behavior.
The view focuses on rendering.
This is composition applied at feature scale — the same idea as Hook composition in Lessons 8 and 14, but with clearer product boundaries.
State Ownership
One of the most important enterprise decisions is:
Where should state live?
Keep state as close as practical to where it is used, while keeping it high enough to serve all required consumers.
That principle was introduced in Hook Best Practices.
At enterprise scale, apply it deliberately.
Component state
Use for UI that belongs to one component:
- input value
- modal visibility
- selected tab
- expanded section
Feature state
Use for workflows shared by a feature:
- checkout steps
- multi-step forms
- product configuration
- dashboard filters
Application state
Use only for values that are truly application-wide:
- authenticated user
- global preferences
- application-wide notifications
- shared permissions
Do not make all state global simply because global state exists.
Component state|vFeature state|vApplication state
Use the smallest scope that solves the problem.
Shared Hooks vs Feature Hooks
Lesson 8 taught Custom Hooks for reusable stateful logic.
Enterprise applications need a sharper distinction.
Shared Hooks
Reusable across unrelated features:
useDebouncedValueuseOnlineStatususeLocalStorageuseMediaQueryuseToggle
Feature Hooks
Specific to a business domain:
useCheckoutuseOrdersuseProductSearchuseUserProfileuseCart
Good names communicate intent:
useAuthuseCurrentUserusePermissionsuseNotificationsuseFeatureFlag
Avoid meaningless abstractions:
useHelperuseCommonuseUtilityuseStuffuseApplicationEverything
Do not force feature-specific logic into a shared Hook just to make the code look reusable.
A good abstraction should provide:
- reuse
- clearer ownership
- simpler components
- meaningful behavior
- a stable API
If extracting a Hook only moves five lines into another file without improving design, keep the code local.
Designing Feature Hook APIs
Lesson 8 covered returning arrays vs objects and keeping Hook APIs clear.
At enterprise scale, the Hook API becomes an internal contract.
Prefer intention-revealing returns:
function useOrders() {// Fetch and manage orders.return {orders,status,error,refresh,createOrder,};}
Avoid leaking implementation details:
return {queryClient,dispatch,internalState,rawResponse,};
Also avoid exposing every setter automatically:
// Too porousreturn {state,setState,cache,setCache,error,setError,};
Expose the operations consumers actually need.
return {data,status,error,refresh,retry,};
Consumers should depend on the API, not the internal architecture.
Once many screens use:
const {orders,status,refresh,} = useOrders();
renaming those fields becomes expensive.
Design Hook APIs deliberately.
Hook Composition at Feature Scale
Hooks can be composed.
Lesson 14 showed simple composition such as useOnlineStatus inside a checkout screen.
Enterprise features often compose several focused Hooks into one feature-level Hook:
function useCheckoutPage() {const auth = useAuth();const cart = useCart();const permissions = usePermissions();const notifications = useNotifications();const analytics = useAnalytics();const checkout = useCheckout({user: auth.user,cart: cart.items,});function startCheckout() {analytics.track("checkout_started", {itemCount: cart.items.length,});checkout.submit();}return {user: auth.user,cart,canCheckout: permissions.can("checkout"),checkout,notifications,startCheckout,};}
Then the page stays readable:
function CheckoutPage() {const {user,cart,canCheckout,checkout,startCheckout,} = useCheckoutPage();if (!canCheckout) {return <p>Checkout unavailable.</p>;}return (<CheckoutViewuser={user}cart={cart}checkout={checkout}onStart={startCheckout}/>);}
Each Hook remains focused.
The feature Hook coordinates them.
This is how you avoid a "God Hook" without forcing every detail into the page component.
Avoid the God Hook
A common enterprise mistake is creating one enormous Hook:
useApplication()
that contains:
- authentication
- cart
- notifications
- settings
- analytics
- routing
- permissions
- API calls
- feature flags
- forms
That becomes a monolithic component in disguise.
Prefer focused Hooks:
useAuth()useCart()useNotifications()useSettings()useAnalytics()usePermissions()useFeatureFlag()
Compose them where a feature needs coordination.
The same advice applies to giant page components with:
20 state variables15 Effects10 callbacksmultiple API calls
Split by responsibility, not by file count.
Context Boundaries
Context solves a value distribution problem.
It does not automatically solve every state-management problem.
A focused auth boundary:
const AuthContext = createContext(null);function AuthProvider({ children }) {const [user, setUser] = useState(null);const value = {user,setUser,};return (<AuthContext.Provider value={value}>{children}</AuthContext.Provider>);}function useAuth() {const context = useContext(AuthContext);if (!context) {throw new Error("useAuth must be used within AuthProvider");}return context;}
Components can then use a clear API:
const { user } = useAuth();
Avoid one enormous Context:
GlobalContext|+-- authentication+-- cart+-- notifications+-- search+-- dashboard filters+-- temporary modal state+-- every application setting
Prefer focused boundaries:
AuthContextCartContextNotificationContextSettingsContext
Or use an appropriate external state-management solution when the application's requirements justify it.
About stabilizing Context values with useMemo: that can help in some architectures, but it is not mandatory.
As Lesson 11 emphasized, measure and reason about actual rendering behavior before adding memoization.
Server State vs UI State
Hook Best Practices already introduced this distinction.
Enterprise architecture depends on treating the two categories differently.
UI state
modalOpenselectedTabsearchTextisSidebarOpen
Owned by the application UI.
Server state
users from APIproducts from APIorders from APIinventory from API
Owned by a remote system and often involving:
- caching
- refetching
- stale data
- retries
- synchronization
- pagination
- invalidation
- deduplication
A simple useEffect can fetch data — Lessons 3, 10, and 14 covered that carefully, including cleanup and race conditions.
Large applications often benefit from a dedicated server-state / data-fetching layer instead of manually rebuilding caching and synchronization with Effects.
Conceptually:
UI state|vowned by application UIServer state|vowned by remote system
The feature Hook can still expose a stable interface either way:
function useOrders() {// Fetch/cache/manage orders through the chosen data layer.return {orders,status,error,refresh,};}
The UI does not need to know whether the implementation uses:
fetch- a query library
- a GraphQL client
- another data layer
That boundary is the enterprise value.
Hook + Service Layer
When API communication becomes substantial, separate it from React state synchronization.
Component|vCustom Hook|vService|vAPI
Service:
export async function getUser(userId, { signal } = {}) {const response = await fetch(`/api/users/${userId}`, {signal,});if (!response.ok) {throw new Error("Failed to load user");}return response.json();}
Hook:
function useUser(userId) {const [state, setState] = useState({status: "loading",data: null,error: null,});useEffect(() => {const controller = new AbortController();getUser(userId, { signal: controller.signal }).then((data) => {setState({status: "success",data,error: null,});}).catch((error) => {if (error.name === "AbortError") {return;}setState({status: "error",data: null,error,});});return () => {controller.abort();};}, [userId]);return state;}
Component:
function UserPage({ userId }) {const { status, data, error } = useUser(userId);// Render UI.}
Notice what this reuses from earlier lessons:
useEffectfor external synchronization- cleanup to avoid stale updates / races
- a status model instead of conflicting booleans
- a Custom Hook API that hides implementation details
The enterprise addition is the service boundary.
The Hook owns React state and lifecycle.
The service owns HTTP communication.
Reducers Inside Feature Hooks
Lesson 7 covered useReducer in depth.
In enterprise features, reducers are especially useful inside Custom Hooks for complex workflows.
Checkout example:
Cart|vAddress|vShipping|vPayment|vConfirmation
function useCheckout() {const [state, dispatch] = useReducer(checkoutReducer,initialCheckoutState);function goToShipping() {dispatch({ type: "shippingSelected" });}function goBack() {dispatch({ type: "back" });}return {...state,goToShipping,goBack,};}
The component does not need to know internal action names.
The Hook exposes a product-facing API.
Keep reducers pure, as Lesson 7 and Best Practices already required:
UI event|vdispatch(action)|vreducer|vnew state
API calls, timers, DOM work, and analytics belong in event handlers, Effects, services, or mutation layers — not inside the reducer.
Authentication Hook
Authentication is a common enterprise boundary.
A useful API:
const {user,status,isAuthenticated,login,logout,} = useAuth();
function Header() {const {user,isAuthenticated,logout,} = useAuth();if (!isAuthenticated) {return <LoginLink />;}return (<header><span>Hello, {user.name}</span><button onClick={logout}>Logout</button></header>);}
The component should not need to understand token storage, session restoration, or transport details.
Lesson 14 showed a simplified useCurrentUser.
At enterprise scale, authentication usually becomes a shared infrastructure Hook or Context boundary reused across many features.
Permissions Hook
Authorization should be expressed clearly.
const {can,hasRole,} = usePermissions();
function AdminPanel() {const { can } = usePermissions();if (!can("manage_users")) {return <p>Access denied.</p>;}return <UserManagement />;}
This is clearer than repeating:
if (user.role === "admin") {// ...}
in many components.
Important:
Frontend permission checks improve user experience. They are not a security boundary.
Sensitive authorization must also be enforced by the backend/server.
Never rely on React alone to protect sensitive operations.
Feature Flags, Analytics, and Notifications
These cross-cutting concerns benefit from small, intention-revealing Hooks.
Feature flags
function Dashboard() {const newDashboardEnabled =useFeatureFlag("new-dashboard");if (newDashboardEnabled) {return <NewDashboard />;}return <LegacyDashboard />;}
Analytics
Trigger analytics from the business event, not from an unrelated Effect:
function CheckoutButton() {const { track } = useAnalytics();function handleCheckout() {track("checkout_started", {source: "cart",});startCheckout();}return (<button onClick={handleCheckout}>Checkout</button>);}
Notifications
function ProfileEditor() {const { success, error } = useNotifications();async function handleSave() {try {await saveProfile();success("Profile saved");} catch {error("Unable to save profile");}}return (<button onClick={handleSave}>Save</button>);}
Call useNotifications() at the top level of the component, or encapsulate the save flow inside a feature Hook.
The key idea is the same:
Cross-cutting infrastructure should expose clear APIs and hide delivery details.
Resource Ownership and External Systems
Lessons 3 and 10 covered Effect cleanup thoroughly.
At enterprise scale, ask one additional question:
Which Hook owns this resource?
Examples:
WebSocketEvent listenerTimerSubscriptionObserverIn-flight requestBrowser API connection
The same boundary that establishes the resource should usually own its cleanup.
function useChatSocket(roomId) {const [messages, setMessages] = useState([]);useEffect(() => {const socket = createChatSocket(roomId);socket.onMessage((message) => {setMessages((current) => [...current,message,]);});return () => {socket.disconnect();};}, [roomId]);return messages;}
The UI only consumes:
const messages = useChatSocket(roomId);
Do not establish a subscription in one place and hope another place remembers to clean it up.
Asynchronous Workflows and Race Conditions
Enterprise async work often involves:
- changing IDs
- rapid filter changes
- overlapping requests
- optimistic updates
- retries
Lesson 14 already showed AbortController for request cancellation.
Apply that habit wherever a Hook synchronizes with remote data.
Also prefer explicit status models:
idleloadingsuccesserror
instead of unrelated booleans that can contradict each other.
A Hook or data layer may also coordinate optimistic updates:
User action|vUpdate UI immediately|vSend request|+---- success --> keep update|+---- failure --> rollback
Optimistic updates are useful when latency hurts UX.
They require a clear rollback strategy.
Do not add them by default to every mutation.
Pagination and Infinite Scroll
Reusable list behavior is a natural enterprise Hook candidate.
Pagination
const {page,nextPage,previousPage,setPage,hasNextPage,} = usePagination({totalPages,});
The Hook owns page transitions.
The UI owns rendering controls.
Infinite scroll
Infinite scrolling usually coordinates:
IntersectionObserver|vnear bottom?|vload next page|vappend results
That combination of browser API + async loading + cleanup is a strong Custom Hook candidate.
Keep the observer and request ownership inside the Hook.
Keep list rendering in the component.
Testing and Dependency Injection
Custom Hooks become easier to test when they expose clear behavior.
Test what the Hook returns and how that return value changes — not private implementation details.
Hard-coded global dependencies can make testing harder.
Instead of tightly coupling a Hook to one global client:
function useOrders() {// always calls one specific global API client}
consider a replaceable boundary:
function useOrders(orderService = defaultOrderService) {// use orderService}
Then tests can provide a controlled implementation.
The exact injection style depends on the application.
The principle is:
Make important external dependencies replaceable where testing requires it.
Also remember:
Data / API error|vHook status / error UIRendering error|vError Boundary
Hooks do not replace Error Boundaries.
They solve different failure modes.
Performance at Architectural Scale
Lesson 11 covered useMemo, useCallback, React.memo, and measurement.
Enterprise performance work starts earlier than memoization:
Observe|vMeasure|vIdentify bottleneck|vOptimize|vMeasure again
Useful architectural levers often matter more than wrapping every function:
- state colocation
- component splitting
- better server-state strategy
- avoiding giant Context values
- virtualization for large lists
- reducing unnecessary derived work
Do not begin with:
useMemo everywhereuseCallback everywhere
If an unstable object or callback causes a real problem, fix the data flow first when possible.
Memoization is a tool, not an architecture.
Enterprise Folder Organization
A feature-oriented structure might look like:
src/features/orders/components/OrderList.jsxOrderDetails.jsxOrderFilters.jsxhooks/useOrders.jsuseOrderDetails.jsuseOrderFilters.jsservices/orderService.jsutils/orderHelpers.js
Shared infrastructure:
src/hooks/useAuth.jsusePermissions.jsuseNotifications.jsuseAnalytics.jsuseDebouncedValue.jsuseOnlineStatus.js
Another team may prefer different conventions.
Both can work.
Choose the structure that makes ownership and discovery clear.
Do not create folders and layers only to look "enterprise."
Practical Architecture Example
A mature order feature might look like:
Order Page|vuseOrderPage()|+----------------------+| |v vuseOrders() useOrderFilters()| |v vData Layer Local UI State|vOrder Service|vBackend API
Supporting infrastructure:
useAuth()usePermissions()useNotifications()useAnalytics()useFeatureFlag()
The goal is not to maximize the number of layers.
The goal is to make responsibilities obvious.
Enterprise Anti-Patterns
Anti-pattern 1: One giant component
Hundreds of lines with many Effects, state variables, and API calls.
Split by responsibility.
Anti-pattern 2: One giant Hook
useApplicationEverything()
Prefer focused Hooks and compose them.
Anti-pattern 3: Everything in Context
Not every state value needs application-wide distribution.
Anti-pattern 4: Effects for business events
If the user clicks Save, handle the save from the event flow.
Do not store a temporary flag only to trigger an Effect.
Anti-pattern 5: Memoization without measurement
Optimization should solve a demonstrated problem.
Anti-pattern 6: Artificial abstraction
Do not create a Hook merely because extraction is possible.
Anti-pattern 7: Duplicated business rules
Centralize genuinely shared rules such as permissions.
Do not centralize every tiny conditional.
Anti-pattern 8: Leaking implementation details
Expose stable, intention-revealing APIs.
Anti-pattern 9: Ignoring server-state complexity
As caching, retries, invalidation, and synchronization grow, manually managing everything with Effects can become expensive.
Anti-pattern 10: Treating frontend permissions as security
Always enforce sensitive authorization on the server.
Enterprise Hook Review Checklist
During design or code review, ask:
Design
- Does this Hook have one clear responsibility?
- Is the name descriptive?
- Is the public API small?
- Are implementation details hidden?
- Is this abstraction earning its complexity?
State
- Is state owned at the correct level?
- Is derived data stored unnecessarily?
- Could local state replace shared state?
Effects
- Is the Effect synchronizing with an external system?
- Are dependencies honest?
- Is cleanup implemented?
- Could overlapping async work produce stale results?
Architecture
- Should this logic belong to a service or data layer?
- Is this a shared Hook or a feature Hook?
- Is Context being used for an appropriate purpose?
- Is server state being treated differently from UI state?
- Are we overengineering for the current complexity?
Performance
- Has the bottleneck been measured?
- Are stable identities actually needed?
- Would colocation or splitting help more than memoization?
Testing
- Can Hook behavior be tested independently?
- Are external dependencies replaceable where necessary?
- Are tests focused on behavior rather than internals?
From Component to Enterprise Architecture
A useful growth path is:
Stage 1 — Local component
useState()
Stage 2 — External synchronization
useEffect()useRef()
Stage 3 — Reusable behavior
useCustomHook()
Stage 4 — Feature architecture
ComponentFeature HookService / Data Layer
Stage 5 — Application architecture
UI|Feature Hooks|Shared infrastructure Hooks / Context|State / Data Layer|Services|External Systems
This progression lets applications grow without forcing enterprise complexity into small features.
Interview questions & answers
Interview preparation
18 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Intermediate
1How would you decide whether state should be component-local, feature-level, or application-wide?
IntermediateModel answer
Start local.
Lift to a feature Hook when several screens in one workflow share behavior.
Make state application-wide only when many unrelated features truly need the same ownership boundary.
2What is the difference between a shared Hook and a feature Hook?
IntermediateModel answer
A shared Hook encapsulates genuinely reusable infrastructure such as auth or notifications.
A feature Hook owns one product workflow—orders, checkout, search—and should not be forced into a generic shared API.
3How would you design a Custom Hook API for a multi-screen feature?
IntermediateModel answer
Expose a small intention-revealing surface: needed state, derived values, and actions.
Hide reducers, Effects, and services inside the Hook so screens stay presentational and the API stays stable as internals evolve.
4When is Context a good fit, and when should it be avoided?
IntermediateModel answer
Context is good for distributing a focused shared value (auth, theme) without prop drilling.
Avoid one giant global Context; prefer narrow providers and do not use Context merely to hide every prop.
5What is the difference between UI state and server state in an enterprise application?
IntermediateModel answer
UI state is ephemeral client interaction state.
Server state is remote data owned by the backend and synchronized into the client.
Mixing them without boundaries makes caching, loading, and invalidation harder.
6Why should a feature Hook often sit above a service or data layer?
IntermediateModel answer
The feature Hook coordinates UI-facing behavior while a service/data layer owns HTTP and persistence details.
That separation keeps Hooks testable and prevents API plumbing from leaking into every component.
7How can Hook composition help without creating a God Hook?
IntermediateModel answer
Compose smaller Hooks with clear responsibilities—filters, details, auth—inside a coordinating feature Hook.
Avoid dumping every concern into one mega Hook that owns unrelated state and Effects.
8How should asynchronous Hooks handle cleanup and race conditions?
IntermediateModel answer
Every subscription, timer, or request the Hook starts must be cleaned up.
Abort or ignore stale responses so only the current dependency snapshot can update state after async work completes.
Advanced
9How would you architect Hooks for a large e-commerce checkout flow?
AdvancedModel answer
Keep presentation components thin; put checkout workflow state and transitions in a feature Hook (often withuseReducer); call payment/order services underneath; and reuse shared Hooks for auth, permissions, and notifications.10How would you prevent a God Hook as a codebase grows?
AdvancedModel answer
Split by responsibility: extract shared infrastructure Hooks, keep feature Hooks focused on one workflow, push I/O to services, and refuse new responsibilities that do not belong to that Hook's ownership boundary.
11How would you design a reusable authentication and permissions boundary?
AdvancedModel answer
Provide auth via a focused Context or shared Hook, and expose permission checks through a small permissions Hook for UX.
Always enforce authorization on the server as the real security boundary.
12Why are frontend permission checks insufficient as a security boundary?
AdvancedModel answer
UI checks only hide or disable controls.
Attackers can call APIs directly, so the backend must authorize every sensitive action regardless of what the React layer shows.
13How would you make a Hook that depends on an API service testable?
AdvancedModel answer
Inject or replace the service/data dependency so tests can stub responses.
Assert the Hook's public behavior—status, data, actions—rather than internal Effect implementation details.
14When would you introduce optimistic updates, and what must they include?
AdvancedModel answer
Use optimistic updates when perceived latency matters and rollback is safe.
Update UI immediately, then reconcile with the server result and include failure handling that restores or corrects state.
15How would you keep Hook APIs stable across a large team and codebase?
AdvancedModel answer
Keep public return shapes small and intention-revealing, hide implementation churn, document ownership, and evolve internals behind the same feature or shared Hook contract.
16How would you decide whether a Hook, Context, or external state library is the right tool?
AdvancedModel answer
Use a Hook for encapsulated behavior, Context for distributing a focused shared value, and an external/server-state library when remote cache, deduping, and invalidation complexity exceeds local Effects.
17How would you review a pull request that introduces a new enterprise Hook?
AdvancedModel answer
Check ownership boundaries, API size, purity of reducers, Effect cleanup, UI vs server-state separation, testability of dependencies, and whether the Hook is genuinely needed versus an extra abstraction layer.
18When should you refuse to add another architectural layer?
AdvancedModel answer
Refuse when complexity does not match the problem—for example wrapping a local toggle in a feature Hook, service, and Context.
Prefer the simplest structure that keeps ownership and data flow obvious.
Practice Project: Enterprise Order Management
Build an order-management feature using an enterprise-style Hook architecture.
Requirements
Support:
- order list
- search
- filtering
- pagination
- order details
- loading state
- error state
- retry
- authentication
- permission checks
- notifications
- analytics event tracking
Suggested structure
features/orders/components/OrderList.jsxOrderFilters.jsxOrderDetails.jsxhooks/useOrders.jsuseOrderDetails.jsuseOrderFilters.jsservices/orderService.js
Shared infrastructure:
hooks/useAuth.jsusePermissions.jsuseNotifications.jsuseAnalytics.js
Challenge
Keep components focused on UI.
Keep reusable stateful behavior inside Hooks.
Keep API communication inside the service/data layer.
Handle request cancellation and stale results correctly.
Do not create abstractions merely for the sake of abstraction.
Do not treat frontend permission checks as the only authorization layer.
Quick Cheat Sheet
| Requirement | Recommended pattern |
|---|---|
| Component-only UI state | Local useState / useReducer |
| Feature workflow | Feature Custom Hook |
| Genuinely shared reusable behavior | Shared Custom Hook |
| Shared value distribution | Focused Context + Hook |
| Remote / server data | Data / server-state layer |
| API communication | Service / data layer |
| Complex transitions | useReducer inside a feature Hook |
| External subscription | Effect + owned cleanup |
| Authentication | Auth Hook / Context |
| Permissions | Permission Hook + server enforcement |
| Analytics | Analytics Hook / service from event handlers |
| Notifications | Notification Hook |
| Feature flags | Feature-flag Hook |
| Race-prone requests | Abort / ignore stale results |
| Performance | Measure first, then optimize |
| Testing | Behavior-focused tests + replaceable dependencies |