React Hooks: A Deep Dive
React Hooks: A Deep Dive
React Hooks revolutionized the way we write React components when they were introduced in React 16.8. They allow you to use state and other React features without writing a class component, making your code more concise, easier to understand, and simpler to test.
Introduction to React Hooks
Before hooks, React components came in two flavors:
- Class components with state and lifecycle methods
- Functional components that were stateless
Hooks brought state management and lifecycle features to functional components, allowing developers to write more concise and reusable code.
React Hooks were introduced in React 16.8, released in February 2019, and have since become the recommended way to write React components.
The Basic Hooks
useState: Managing Component State
The useState hook allows functional components to manage state:
1import React, { useState } from 'react';
2
3function Counter() {
4 // Declare a state variable 'count' with initial value 0
5 const [count, setCount] = useState(0);
6
7 return (
8 <div>
9 <p>You clicked {count} times</p>
10 <button onClick={() => setCount(count + 1)}>
11 Click me
12 </button>
13 </div>
14 );
15}
16Key points about useState:
- Returns a pair: the current state value and a function to update it
- The update function can take a new value or a function that receives the previous state
- State updates trigger re-renders
useEffect: Side Effects in Functional Components
The useEffect hook handles side effects in functional components:
1import React, { useState, useEffect } from 'react';
2
3function DocumentTitleUpdater() {
4 const [count, setCount] = useState(0);
5
6 // Update document title using the browser API
7 useEffect(() => {
8 document.title = `You clicked ${count} times`;
9
10 // Optional cleanup function
11 return () => {
12 document.title = 'React App';
13 };
14 }, [count]); // Only re-run if count changes
15
16 return (
17 <div>
18 <p>You clicked {count} times</p>
19 <button onClick={() => setCount(count + 1)}>
20 Click me
21 </button>
22 </div>
23 );
24}
25Key points about useEffect:
- Runs after every render by default
- The dependency array controls when the effect runs
- Returns an optional cleanup function
- Replaces
componentDidMount,componentDidUpdate, andcomponentWillUnmount
useContext: Consuming Context
The useContext hook provides a way to consume React context:
1import React, { useContext } from 'react';
2
3// Create a context
4const ThemeContext = React.createContext('light');
5
6function ThemedButton() {
7 // Consume the context
8 const theme = useContext(ThemeContext);
9
10 return (
11 <button style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}>
12 I am styled based on the theme context!
13 </button>
14 );
15}
16
17function App() {
18 return (
19 <ThemeContext.Provider value="dark">
20 <ThemedButton />
21 </ThemeContext.Provider>
22 );
23}
24Key points about useContext:
- Accepts a context object created by
React.createContext - Returns the current context value
- Always re-renders when the context value changes
Additional Hooks
useReducer: Complex State Logic
The useReducer hook is an alternative to useState for complex state logic:
1import React, { useReducer } from 'react';
2
3// Reducer function
4function counterReducer(state, action) {
5 switch (action.type) {
6 case 'increment':
7 return { count: state.count + 1 };
8 case 'decrement':
9 return { count: state.count - 1 };
10 case 'reset':
11 return { count: 0 };
12 default:
13 throw new Error(`Unsupported action type: ${action.type}`);
14 }
15}
16
17function Counter() {
18 // Initialize state with useReducer
19 const [state, dispatch] = useReducer(counterReducer, { count: 0 });
20
21 return (
22 <div>
23 <p>Count: {state.count}</p>
24 <button onClick={() => dispatch({ type: 'increment' })}>+</button>
25 <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
26 <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
27 </div>
28 );
29}
30Key points about useReducer:
- Takes a reducer function and initial state
- Returns the current state and a dispatch function
- Useful for complex state logic or when next state depends on previous state
- Can optimize performance for components that trigger deep updates
useCallback: Memoized Callbacks
The useCallback hook returns a memoized callback function:
1import React, { useState, useCallback } from 'react';
2
3function ParentComponent() {
4 const [count, setCount] = useState(0);
5
6 // This function is recreated only when count changes
7 const handleClick = useCallback(() => {
8 console.log(`Button clicked, count: ${count}`);
9 }, [count]);
10
11 return (
12 <div>
13 <p>Count: {count}</p>
14 <button onClick={() => setCount(count + 1)}>Increment</button>
15 <ChildComponent onClick={handleClick} />
16 </div>
17 );
18}
19
20// Child component that uses React.memo to prevent unnecessary renders
21const ChildComponent = React.memo(({ onClick }) => {
22 console.log('ChildComponent rendered');
23 return <button onClick={onClick}>Click me</button>;
24});
25Key points about useCallback:
- Returns a memoized version of the callback
- Only changes if one of the dependencies has changed
- Useful when passing callbacks to optimized child components that rely on reference equality
useMemo: Memoized Values
The useMemo hook returns a memoized value:
1import React, { useState, useMemo } from 'react';
2
3function ExpensiveCalculation({ list }) {
4 const [filter, setFilter] = useState('');
5
6 // This calculation is only performed when list or filter changes
7 const filteredList = useMemo(() => {
8 console.log('Filtering list...');
9 return list.filter(item => item.includes(filter));
10 }, [list, filter]);
11
12 return (
13 <div>
14 <input
15 value={filter}
16 onChange={e => setFilter(e.target.value)}
17 placeholder="Filter items..."
18 />
19 <ul>
20 {filteredList.map((item, index) => (
21 <li key={index}>{item}</li>
22 ))}
23 </ul>
24 </div>
25 );
26}
27Key points about useMemo:
- Returns a memoized value
- Only recomputes the value when dependencies change
- Useful for expensive calculations or to maintain referential equality
useRef: Persistent Mutable Values
The useRef hook creates a mutable reference that persists across renders:
1import React, { useRef, useEffect } from 'react';
2
3function FocusInput() {
4 // Create a ref
5 const inputRef = useRef(null);
6
7 // Focus the input on mount
8 useEffect(() => {
9 inputRef.current.focus();
10 }, []);
11
12 return (
13 <div>
14 <input ref={inputRef} type="text" placeholder="I'll be focused on mount" />
15 </div>
16 );
17}
18Key points about useRef:
- Returns a mutable ref object with a
.currentproperty - The ref object persists for the full lifetime of the component
- Changes to
.currentdon't trigger re-renders - Useful for accessing DOM elements or storing mutable values
Creating Custom Hooks
One of the most powerful features of hooks is the ability to create custom hooks that encapsulate reusable logic:
Example: useLocalStorage
1import { useState, useEffect } from 'react';
2
3// Custom hook for persisting state to localStorage
4function useLocalStorage(key, initialValue) {
5 // Initialize state with value from localStorage or initialValue
6 const [storedValue, setStoredValue] = useState(() => {
7 try {
8 const item = window.localStorage.getItem(key);
9 return item ? JSON.parse(item) : initialValue;
10 } catch (error) {
11 console.error(error);
12 return initialValue;
13 }
14 });
15
16 // Update localStorage when state changes
17 useEffect(() => {
18 try {
19 window.localStorage.setItem(key, JSON.stringify(storedValue));
20 } catch (error) {
21 console.error(error);
22 }
23 }, [key, storedValue]);
24
25 return [storedValue, setStoredValue];
26}
27
28// Usage
29function App() {
30 const [name, setName] = useLocalStorage('name', 'Guest');
31
32 return (
33 <div>
34 <input
35 value={name}
36 onChange={e => setName(e.target.value)}
37 placeholder="Enter your name"
38 />
39 <p>Hello, {name}!</p>
40 </div>
41 );
42}
43Example: useFetch
1import { useState, useEffect } from 'react';
2
3// Custom hook for fetching data
4function useFetch(url) {
5 const [data, setData] = useState(null);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState(null);
8
9 useEffect(() => {
10 let isMounted = true;
11
12 async function fetchData() {
13 try {
14 setLoading(true);
15 const response = await fetch(url);
16
17 if (!response.ok) {
18 throw new Error(`HTTP error! Status: ${response.status}`);
19 }
20
21 const result = await response.json();
22
23 if (isMounted) {
24 setData(result);
25 setError(null);
26 }
27 } catch (error) {
28 if (isMounted) {
29 setError(error.message);
30 setData(null);
31 }
32 } finally {
33 if (isMounted) {
34 setLoading(false);
35 }
36 }
37 }
38
39 fetchData();
40
41 return () => {
42 isMounted = false;
43 };
44 }, [url]);
45
46 return { data, loading, error };
47}
48
49// Usage
50function UserProfile({ userId }) {
51 const { data, loading, error } = useFetch(`https://api.example.com/users/${userId}`);
52
53 if (loading) return <p>Loading...</p>;
54 if (error) return <p>Error: {error}</p>;
55
56 return (
57 <div>
58 <h1>{data.name}</h1>
59 <p>Email: {data.email}</p>
60 </div>
61 );
62}
63Best Practices and Common Pitfalls
Rules of Hooks
React Hooks come with two essential rules:
-
Only call hooks at the top level
- Don't call hooks inside loops, conditions, or nested functions
- This ensures hooks are called in the same order each render
-
Only call hooks from React functions
- Call hooks from React functional components
- Call hooks from custom hooks
1// ❌ Wrong: Hook inside a condition
2function MyComponent() {
3 const [count, setCount] = useState(0);
4
5 if (count > 0) {
6 useEffect(() => {
7 document.title = `Count: ${count}`;
8 });
9 }
10
11 return <button onClick={() => setCount(count + 1)}>Increment</button>;
12}
13
14// ✅ Correct: Condition inside the hook
15function MyComponent() {
16 const [count, setCount] = useState(0);
17
18 useEffect(() => {
19 if (count > 0) {
20 document.title = `Count: ${count}`;
21 }
22 }, [count]);
23
24 return <button onClick={() => setCount(count + 1)}>Increment</button>;
25}
26Common Pitfalls
Stale Closures
1function Counter() {
2 const [count, setCount] = useState(0);
3
4 // This will always alert 0!
5 const handleAlertClick = () => {
6 setTimeout(() => {
7 alert('You clicked on: ' + count);
8 }, 3000);
9 };
10
11 return (
12 <div>
13 <p>You clicked {count} times</p>
14 <button onClick={() => setCount(count + 1)}>Click me</button>
15 <button onClick={handleAlertClick}>Show alert</button>
16 </div>
17 );
18}
19Solution: Use a function update or useRef:
1function Counter() {
2 const [count, setCount] = useState(0);
3 const countRef = useRef(count);
4
5 // Update ref when count changes
6 useEffect(() => {
7 countRef.current = count;
8 }, [count]);
9
10 const handleAlertClick = () => {
11 setTimeout(() => {
12 alert('You clicked on: ' + countRef.current);
13 }, 3000);
14 };
15
16 return (
17 <div>
18 <p>You clicked {count} times</p>
19 <button onClick={() => setCount(count + 1)}>Click me</button>
20 <button onClick={handleAlertClick}>Show alert</button>
21 </div>
22 );
23}
24Missing Dependencies
1function SearchResults({ query }) {
2 const [results, setResults] = useState([]);
3
4 // ❌ Missing dependency: query
5 useEffect(() => {
6 fetchResults(query).then(data => {
7 setResults(data);
8 });
9 }, []); // This effect runs only once, not when query changes
10
11 return (
12 <ul>
13 {results.map(result => (
14 <li key={result.id}>{result.title}</li>
15 ))}
16 </ul>
17 );
18}
19Solution: Add all dependencies:
1function SearchResults({ query }) {
2 const [results, setResults] = useState([]);
3
4 // ✅ Correct: query is included in dependencies
5 useEffect(() => {
6 fetchResults(query).then(data => {
7 setResults(data);
8 });
9 }, [query]); // This effect runs when query changes
10
11 return (
12 <ul>
13 {results.map(result => (
14 <li key={result.id}>{result.title}</li>
15 ))}
16 </ul>
17 );
18}
19Advanced Patterns with Hooks
State Machines with useReducer
1import React, { useReducer } from 'react';
2
3// Define the state machine
4const initialState = { status: 'idle', data: null, error: null };
5
6function fetchReducer(state, action) {
7 switch (action.type) {
8 case 'FETCH_START':
9 return { status: 'loading', data: null, error: null };
10 case 'FETCH_SUCCESS':
11 return { status: 'success', data: action.payload, error: null };
12 case 'FETCH_ERROR':
13 return { status: 'error', data: null, error: action.payload };
14 case 'RESET':
15 return initialState;
16 default:
17 throw new Error(`Unsupported action type: ${action.type}`);
18 }
19}
20
21function DataFetcher({ url }) {
22 const [state, dispatch] = useReducer(fetchReducer, initialState);
23
24 const fetchData = async () => {
25 dispatch({ type: 'FETCH_START' });
26
27 try {
28 const response = await fetch(url);
29
30 if (!response.ok) {
31 throw new Error(`HTTP error! Status: ${response.status}`);
32 }
33
34 const data = await response.json();
35 dispatch({ type: 'FETCH_SUCCESS', payload: data });
36 } catch (error) {
37 dispatch({ type: 'FETCH_ERROR', payload: error.message });
38 }
39 };
40
41 return (
42 <div>
43 <button onClick={fetchData} disabled={state.status === 'loading'}>
44 {state.status === 'loading' ? 'Loading...' : 'Fetch Data'}
45 </button>
46
47 {state.status === 'success' && (
48 <div>
49 <h2>Data:</h2>
50 <pre>{JSON.stringify(state.data, null, 2)}</pre>
51 </div>
52 )}
53
54 {state.status === 'error' && (
55 <div>
56 <h2>Error:</h2>
57 <p>{state.error}</p>
58 </div>
59 )}
60 </div>
61 );
62}
63Compound Components with Context
1import React, { createContext, useContext, useState } from 'react';
2
3// Create context
4const TabsContext = createContext();
5
6// Tabs component
7function Tabs({ children, defaultIndex = 0 }) {
8 const [activeIndex, setActiveIndex] = useState(defaultIndex);
9
10 return (
11 <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
12 <div className="tabs">{children}</div>
13 </TabsContext.Provider>
14 );
15}
16
17// TabList component
18function TabList({ children }) {
19 return <div className="tab-list">{children}</div>;
20}
21
22// Tab component
23function Tab({ children, index }) {
24 const { activeIndex, setActiveIndex } = useContext(TabsContext);
25 const isActive = activeIndex === index;
26
27 return (
28 <button
29 className={`tab ${isActive ? 'active' : ''}`}
30 onClick={() => setActiveIndex(index)}
31 >
32 {children}
33 </button>
34 );
35}
36
37// TabPanels component
38function TabPanels({ children }) {
39 return <div className="tab-panels">{children}</div>;
40}
41
42// TabPanel component
43function TabPanel({ children, index }) {
44 const { activeIndex } = useContext(TabsContext);
45
46 if (activeIndex !== index) return null;
47
48 return <div className="tab-panel">{children}</div>;
49}
50
51// Compose the components
52Tabs.TabList = TabList;
53Tabs.Tab = Tab;
54Tabs.TabPanels = TabPanels;
55Tabs.TabPanel = TabPanel;
56
57// Usage
58function App() {
59 return (
60 <Tabs>
61 <Tabs.TabList>
62 <Tabs.Tab index={0}>Tab 1</Tabs.Tab>
63 <Tabs.Tab index={1}>Tab 2</Tabs.Tab>
64 <Tabs.Tab index={2}>Tab 3</Tabs.Tab>
65 </Tabs.TabList>
66
67 <Tabs.TabPanels>
68 <Tabs.TabPanel index={0}>Content for Tab 1</Tabs.TabPanel>
69 <Tabs.TabPanel index={1}>Content for Tab 2</Tabs.TabPanel>
70 <Tabs.TabPanel index={2}>Content for Tab 3</Tabs.TabPanel>
71 </Tabs.TabPanels>
72 </Tabs>
73 );
74}
75Conclusion
React Hooks have transformed how we write React components, making it easier to reuse stateful logic, organize code by concern rather than lifecycle methods, and avoid the complexity of class components.
By mastering hooks, you can write more concise, maintainable, and reusable React code. Start by understanding the basic hooks, then explore creating custom hooks to encapsulate and share logic across your application.
As you continue your React journey, remember that hooks are just tools to help you build better components. The principles of good component design—such as single responsibility, encapsulation, and composition—still apply. Hooks simply make it easier to implement these principles in your React applications.