React Hooks Deep Dive
- 1.Practical React + TypeScript Tips
- 2.React Hooks Deep Dive
React Hooks are a core feature that lets function components handle state and side effects. This post looks at how common Hooks work and the patterns that are useful in practice.
Understanding useState
useState is the most basic Hook. It adds state to a component and triggers a re-render when that state changes.
const [count, setCount] = useState(0)
Passing a Function as the Initial Value
If calculating the initial value is expensive, pass a function. React will run it only during the initial render.
const [data, setData] = useState(() => {
return expensiveComputation()
})
Functional Updates
When the next state depends on the previous state, use a functional update. This is especially useful for avoiding stale closure problems in asynchronous situations.
setCount(prev => prev + 1)
Using useEffect Correctly
useEffect handles side effects in a component. API calls, DOM manipulation, and subscriptions are common examples.
useEffect(() => {
const controller = new AbortController()
fetch("/api/data", { signal: controller.signal })
.then(res => res.json())
.then(setData)
return () => controller.abort()
}, [])
Dependency Array Pitfalls
Putting objects or arrays directly into the dependency array can create a new reference on every render and cause an infinite loop.
// Bad example - runs on every render
useEffect(() => {
fetchData(options)
}, [options]) // options is a new object every time
// Good example - split into individual values
useEffect(() => {
fetchData({ page, limit })
}, [page, limit])
Why Cleanup Matters
If subscriptions or timers are not cleaned up when a component unmounts, memory leaks can occur. The cleanup function runs before the next effect and when the component unmounts.
useEffect(() => {
const timer = setInterval(() => {
setSeconds(s => s + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
useMemo and useCallback
These two Hooks are used for render optimization. They prevent unnecessary recalculation or unnecessary re-renders of child components.
useMemo - Memoizing Values
useMemo caches the result of an expensive calculation. If dependencies do not change, React reuses the previous result.
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name))
}, [items])
useCallback - Memoizing Functions
useCallback preserves function reference identity. It is useful when passing callbacks to child components wrapped with React.memo.
const handleClick = useCallback((id: string) => {
setSelected(id)
}, [])
When Should You Use Them?
Memoizing every value and function can hurt performance instead of helping. Use them mainly in these cases:
- Expensive calculations such as sorting, filtering, or transforming
- Props passed to children wrapped with
React.memo - Values used as dependencies of other Hooks
useRef Patterns
useRef is a container that stores a mutable value in its .current property. The key difference from useState is that changing a ref does not trigger a re-render.
DOM Access
The most common use case is direct access to a DOM element.
const inputRef = useRef<HTMLInputElement>(null)
function focusInput() {
inputRef.current?.focus()
}
return <input ref={inputRef} />
Remembering a Previous Value
Refs are useful when you need to track a previous value across renders.
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
})
return ref.current
}
Building Custom Hooks
Custom Hooks make logic reusable. They are functions that start with use, and they can call other Hooks internally.
useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initialValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue] as const
}
useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debounced
}
useMediaQuery
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false)
useEffect(() => {
const media = window.matchMedia(query)
setMatches(media.matches)
function handler(e: MediaQueryListEvent) {
setMatches(e.matches)
}
media.addEventListener("change", handler)
return () => media.removeEventListener("change", handler)
}, [query])
return matches
}
Closing
React Hooks look simple, but using them correctly requires understanding closures, reference identity, and the rendering cycle. The important part is knowing each Hook's characteristics and using it in the right situation.