React Hooks are easier to choose when you classify the problem first. Use useState for simple component state, useReducer for complex state transitions, useEffect for synchronizing with external systems, and useContext for shared values across a component tree. Extract a custom Hook only when the logic is genuinely reusable.
The two basic Hook rules
Hooks must be called at the top level of a React function component or custom Hook:
function SearchPanel({ enabled }: { enabled: boolean }) {
const [query, setQuery] = useState("")
if (!enabled) return null
return <input value={query} onChange={(event) => setQuery(event.target.value)} />
}
Do not call Hooks inside conditions, loops, event handlers, or ordinary utility functions. React relies on stable call order to associate each Hook with its state. Keep eslint-plugin-react-hooks enabled.
useState: simple local state
Use useState for inputs, toggles, selected items, counters, and other local UI values:
import { useState } from "react"
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount((current) => current + 1)}>
Clicked {count} times
</button>
)
}
Use functional updates when the new value depends on the previous value. Do not mutate objects or arrays in place:
setProfile((profile) => ({ ...profile, name: "Steven" }))
setTools((tools) => [...tools, "Raycast"])
Values that can be derived from props or other state often do not need their own state. Mutable values that do not affect rendering may belong in useRef.
useEffect: external synchronization only
useEffect is for synchronizing a component with something outside React: browser APIs, subscriptions, timers, network connections, or third-party widgets. It is not a general "run code after render" container.
import { useEffect, useState } from "react"
export function OnlineStatus() {
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 <span>{online ? "Online" : "Offline"}</span>
}
The decision flow should be: which external system needs synchronization, how do we start it, and how do we stop it when dependencies change or the component unmounts?
Avoid request races
For client-side fetches triggered by user input, cancel outdated requests:
useEffect(() => {
const controller = new AbortController()
async function load() {
const response = await fetch(`/api/tools?q=${query}`, {
signal: controller.signal,
})
const result = await response.json()
setTools(result)
}
load().catch((error) => {
if (error.name !== "AbortError") console.error(error)
})
return () => controller.abort()
}, [query])
In Next.js, first-screen data usually belongs in Server Components or the framework data layer. Client Effects are better for browser-only APIs, interaction-driven requests, and live subscriptions. See Next.js Server vs Client Components.
useReducer: complex state transitions
Use a reducer when state has multiple related fields, many actions, or a workflow that benefits from named transitions:
import { useReducer } from "react"
type State = { status: "idle" | "saving" | "saved"; error?: string }
type Action =
| { type: "save_started" }
| { type: "save_succeeded" }
| { type: "save_failed"; message: string }
function reducer(state: State, action: Action): State {
switch (action.type) {
case "save_started":
return { status: "saving" }
case "save_succeeded":
return { status: "saved" }
case "save_failed":
return { status: "idle", error: action.message }
default:
return state
}
}
Reducers must stay pure. Do not fetch data, write localStorage, or mutate the existing state inside the reducer.
useContext: shared values, not a full state manager
Context solves prop drilling for values such as theme, language, current user, or dependency injection:
import { createContext, useContext, useState } from "react"
type Theme = "light" | "dark"
const ThemeContext = createContext<Theme | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme] = useState<Theme>("dark")
return <ThemeContext value={theme}>{children}</ThemeContext>
}
export function useTheme() {
const value = useContext(ThemeContext)
if (value === null) throw new Error("useTheme must be used inside ThemeProvider")
return value
}
When a Provider value changes, consumers re-render. Avoid putting one large, frequently changing object into a single Context.
Custom Hooks
Custom Hooks reuse stateful logic, not state instances:
import { useEffect, useState } from "react"
export function useDebouncedValue<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = window.setTimeout(() => setDebounced(value), delay)
return () => window.clearTimeout(timer)
}, [value, delay])
return debounced
}
Good custom Hooks express a concrete capability, such as useOnlineStatus or useDebouncedValue. Do not wrap code just to make a component shorter.
Related FreeMac guides
- For component boundaries, read React Component Design: Composition, Abstraction, and Reuse.
- For Next.js server-client boundaries, read Next.js Server vs Client Components.
- For hydration issues, see React Hydration Failed: Causes, Debugging, and Fixes.
Continue reading
React Portal: Render Modals Outside Parent DOM
Use React Portal for modals, dropdowns, tooltips, drawers, and toasts that must escape overflow, stacking context, or local layout boundaries.
11 min readReact Component Design: Composition, Abstraction, and Reuse
Design React components with clear APIs, composition, controlled inputs, local state boundaries, custom Hooks, and fewer boolean props.
12 min readSide Effects in Vue 3 and React: Watchers, Effects, and Cleanup
Compare side effect handling in Vue 3 and React, including API requests, event listeners, timers, AbortController, watchers, useEffect dependencies, and cleanup.
Subscribe to FreeMac
Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.