logo
A Deep Dive into React Hooks
Back to all articles
React2024-12-0210 min read

A Deep Dive into React Hooks

Master React Hooks with a comprehensive guide to useState, useEffect, useContext, useReducer, useMemo, useCallback, and custom hooks.

Francis Njenga

Francis Njenga

Lead Developer

A Deep Dive into React Hooks

React Hooks, introduced in React 16.8, revolutionized how developers write and organize React components. They allow you to use state and other React features without writing classes.

Key Benefits of Hooks

  • Reuse stateful logic between components
  • Organize related code together
  • Simplify complex components
  • Use React features without classes
  • Better performance optimization

Why Hooks Matter

Before Hooks, React developers had to choose between function components (simpler but limited) and class components (more powerful but complex). Hooks bring the power of class components to function components.

Core React Hooks

useState

Manages local component state

useEffect

Handles side effects

useContext

Accesses context values

useReducer

Manages complex state logic

Performance Optimization Hooks

  • useMemo: Memoizes expensive calculations
  • useCallback: Memoizes callback functions
  • useRef: Persists values across renders

Custom Hooks

One of the most powerful features of Hooks is the ability to create your own custom hooks that encapsulate reusable logic.

Rules of Hooks

Essential Rules to Follow

  • Only call hooks at the top level
  • Only call hooks from React functions
  • Name custom hooks starting with "use"

Best Practices

  • Keep custom hooks focused on single responsibilities
  • Specify all dependencies in dependency arrays
  • Extract complex logic into custom hooks
  • Use the React DevTools profiler for optimization

Conclusion

React Hooks represent a significant step forward in React's evolution, making it easier to write and maintain components while encouraging better patterns for code organization and reuse.

By mastering hooks, you can write more concise, maintainable, and powerful React applications that are easier to test and reason about.

useState Hook
Managing component state with useState
useState-example.jsx
function Counter() {
        const [count, setCount] = useState(0);
        const [isActive, setIsActive] = useState(false);
        const [user, setUser] = useState({ name: '', email: '' });
      
        const updateEmail = (newEmail) => {
          setUser({
            ...user,
            email: newEmail
          });
        };
      
        return (
          <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            
            <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
            <button onClick={() => setIsActive(!isActive)}>Toggle</button>
            
            <div>
              <input
                value={user.name}
                onChange={(e) => setUser({...user, name: e.target.value})}
                placeholder="Name"
              />
              <input
                value={user.email}
                onChange={(e) => updateEmail(e.target.value)}
                placeholder="Email"
              />
            </div>
          </div>
        );
      }

Using useState to manage various state types

useEffect Hook
Handling side effects with useEffect
useEffect-example.jsx
function DataFetcher() {
        const [data, setData] = useState(null);
        const [loading, setLoading] = useState(true);
        const [error, setError] = useState(null);
        const [userId, setUserId] = useState(1);
      
        useEffect(() => {
          setLoading(true);
          setError(null);
      
          async function fetchData() {
            try {
              const response = await fetch(
                 `https://jsonplaceholder.typicode.com/users/${userId}`
              );
              const userData = await response.json();
              setData(userData);
            } catch (err) {
              setError(err.message);
            } finally {
              setLoading(false);
            }
          }
      
          fetchData();
      
          return () => {
            console.log('Cleanup before fetching new data');
          };
        }, [userId]);
      
        return (
          <div>
            <button onClick={() => setUserId(id => Math.max(1, id - 1))}>
              Previous User
            </button>
            <span> User ID: {userId} </span>
            <button onClick={() => setUserId(id => id + 1)}>
              Next User
            </button>
      
            {loading && <p>Loading...</p>}
            {error && <p>Error: {error}</p>}
            {data && (
              <div>
                <h3>{data.name}</h3>
                <p>Email: {data.email}</p>
              </div>
            )}
          </div>
        );
      }

Data fetching with useEffect and cleanup

useContext Hook
Consuming context with useContext
useContext-example.jsx
const ThemeContext = createContext('light');
      
      function ThemedButton() {
        const theme = useContext(ThemeContext);
      
        return (
          <button style={{ 
            background: theme === 'dark' ? '#333' : '#fff',
            color: theme === 'dark' ? '#fff' : '#333'
          }}>
            I'm styled by context!
          </button>
        );
      }
      
      function App() {
        return (
          <ThemeContext.Provider value="dark">
            <ThemedButton />
          </ThemeContext.Provider>
        );
      }

Using context in function components

useReducer Hook
Managing complex state with useReducer
useReducer-example.jsx
function counterReducer(state, action) {
        switch (action.type) {
          case 'increment':
            return { count: state.count + 1 };
          case 'decrement':
            return { count: state.count - 1 };
          case 'reset':
            return { count: 0 };
          default:
            throw new Error();
        }
      }
      
      function Counter() {
        const [state, dispatch] = useReducer(counterReducer, { count: 0 });
      
        return (
          <div>
            Count: {state.count}
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
            <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
          </div>
        );
      }

Redux-like state management with useReducer

useMemo & useCallback
Performance optimization hooks
memoization-hooks.jsx

                function ExpensiveComponent({ a, b }) {
        const result = useMemo(() => {
          console.log('Calculating...');
          return a * b;
        }, [a, b]);
      
        const handleClick = useCallback(() => {
          console.log('Result:', result);
        }, [result]);
      
        return (
          <div>
            <p>Result: {result}</p>
            <button onClick={handleClick}>Log Result</button>
          </div>
        );
      }

Optimizing performance with memoization

useRef Hook
Accessing DOM and storing mutable values
useRef-example.jsx
function TextInput() {
        const inputRef = useRef(null);
        const [value, setValue] = useState('');
        const prevValue = useRef('');
      
        useEffect(() => {
          prevValue.current = value;
        }, [value]);
      
        const focusInput = () => {
          inputRef.current.focus();
        };
      
        return (
          <div>
            <input 
              ref={inputRef}
              value={value}
              onChange={(e) => setValue(e.target.value)}
            />
            <button onClick={focusInput}>Focus Input</button>
            <p>Current: {value}, Previous: {prevValue.current}</p>
          </div>
        );
      }

Using useRef for DOM access and value persistence

Custom Hooks
Creating reusable logic with custom hooks
custom-hook-example.jsx
function useLocalStorage(key, initialValue) {
        const [storedValue, setStoredValue] = useState(() => {
          try {
            const item = window.localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
          } catch (error) {
            return initialValue;
          }
        });
      
        const setValue = (value) => {
          try {
            const valueToStore = value instanceof Function ? value(storedValue) : value;
            setStoredValue(valueToStore);
            window.localStorage.setItem(key, JSON.stringify(valueToStore));
          } catch (error) {
            console.error(error);
          }
        };
      
        return [storedValue, setValue];
      }
      
      function LoginForm() {
        const [username, setUsername] = useLocalStorage('username', '');
        const [password, setPassword] = useLocalStorage('password', '');
      
        return (
          <form>
            <input 
              value={username}
              onChange={(e) => setUsername(e.target.value)}
              placeholder="Username"
            />
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              placeholder="Password"
            />
          </form>
        );
      }

Creating and using a custom localStorage hook

Francis Njenga

Francis Njenga

Lead Developer

Francis Njenga is an experienced Lead Developer specializing in React, Next.js, and modern JavaScript frameworks, with a strong background in web development.

You might also like

;