Post

Mastering React Hooks for 2025

Mastering React Hooks for 2025

React Hooks have fundamentally transformed how we build React applications since their introduction in React 16.8. With React 18 and beyond, hooks have become even more powerful and essential for modern React development. In this post lets explores everything we need to know about hooks, from fundamental concepts to advanced patterns and the latest features introduced with latest React releases.


What Are React Hooks?

React Hooks are functions that let you “hook into” React features like state and lifecycle methods from functional components. They provide a more direct way to use React features without the complexity of class components, enabling better code reuse and composition. React hooks embrace functional programming approaches by powering up functional components.

Rules of Thumbs for React Hooks

Before diving in, remember the fundamental rules for react hooks to avoid undesired behaviours and performance degregation of application:

  1. Only call hooks at the top level - Never inside loops, conditions, or nested functions
  2. Only call hooks from React functions - React functional components or custom hooks
  3. Hook names must start with “use” - This helps React’s linter enforce the rules

Build-in react hooks

There are number of essential build-in react hooks we are using day to day react applications. These hook functionalites are primary building blocks of react UI design.

useState: Managing Component State

The useState hook adds state to functional components with a clean, declarative API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // Functional updates for complex state changes
  const increment = () => setCount(prevCount => prevCount + 1);
  
  // Object state updates (remember to spread!)
  const [user, setUser] = useState({ name: '', email: '' });
  const updateUserName = (newName) => 
    setUser(prevUser => ({ ...prevUser, name: newName }));

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      
      <input 
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter your name"
      />
    </div>
  );
}

Best Practices:

  • Use functional updates when the new state depends on the previous state
  • Separate related state variables for better organization
  • Consider useReducer for complex state logic

useEffect: Side Effects and Lifecycle

The useEffect hook handles side effects and replaces lifecycle methods from class components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
          throw new Error('Failed to fetch user');
        }
        
        const userData = await response.json();
        
        // Prevent state updates if component unmounted
        if (!cancelled) {
          setUser(userData);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
          setUser(null);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    // Cleanup function
    return () => {
      cancelled = true;
    };
  }, [userId]); // Dependency array

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

useReducer: Complex State Logic

For more complex state logic, useReducer provides better organization and predictability.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React, { useReducer } from 'react';

const initialState = {
  items: [],
  filter: 'all',
  loading: false,
  error: null
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, { id: Date.now(), text: action.text, completed: false }]
      };
    case 'TOGGLE_ITEM':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.id ? { ...item, completed: !item.completed } : item
        )
      };
    case 'SET_FILTER':
      return { ...state, filter: action.filter };
    case 'SET_LOADING':
      return { ...state, loading: action.loading };
    case 'SET_ERROR':
      return { ...state, error: action.error };
    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  
  const addItem = (text) => {
    dispatch({ type: 'ADD_ITEM', text });
  };
  
  const toggleItem = (id) => {
    dispatch({ type: 'TOGGLE_ITEM', id });
  };

  const filteredItems = state.items.filter(item => {
    if (state.filter === 'completed') return item.completed;
    if (state.filter === 'active') return !item.completed;
    return true;
  });

  return (
    <div>
      <h1>Todo App</h1>
      {/* App implementation */}
    </div>
  );
}

useContext: Sharing Data Across Components

useContext provides a cleaner way to consume context values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();
const UserContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={ {theme, toggleTheme} }>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <button
      onClick={toggleTheme}
      className={`btn btn-${theme}`}
    >
      Switch to {theme === 'light' ? 'dark' : 'light'} theme
    </button>
  );
}

Advanced Built-in Hooks

Advanced react hooks are used in specialised cases and will not be needed in many day-to-day user interface functionalities. But they are essential to achieve the requried behaviours when needed without using bloated code.

useRef: Accessing DOM Elements and Persisting Values

useRef serves two main purposes: accessing DOM elements and storing mutable values that don’t trigger re-renders.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { useRef, useEffect, useState } from 'react';

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const renderCount = useRef(0);
  const [text, setText] = useState('');

  useEffect(() => {
    renderCount.current += 1;
  });

  const onButtonClick = () => {
    // Access DOM element directly
    inputEl.current.focus();
  };

  return (
    <div>
      <input
        ref={inputEl}
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button onClick={onButtonClick}>Focus the input</button>
      <p>Component rendered {renderCount.current} times</p>
    </div>
  );
}

useLayoutEffect: Synchronous Side Effects

useLayoutEffect runs synchronously after all DOM mutations, useful for measuring DOM elements.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useState, useLayoutEffect, useRef } from 'react';

function DynamicTooltip({ children, tooltip }) {
  const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
  const tooltipRef = useRef(null);
  const triggerRef = useRef(null);

  useLayoutEffect(() => {
    if (tooltipRef.current && triggerRef.current) {
      const triggerRect = triggerRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();
      
      setTooltipPosition({
        x: triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2,
        y: triggerRect.top - tooltipRect.height - 8
      });
    }
  }, [tooltip]);

  return (
    <div className="tooltip-container">
      <div ref={triggerRef}>
        {children}
      </div>
      <div
        ref={tooltipRef}
        className="tooltip"
        style=
      >
        {tooltip}
      </div>
    </div>
  );
}

useImperativeHandle: Customizing Instance Values

Rarely used, but useful for exposing imperative API to parent components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { forwardRef, useImperativeHandle, useRef } from 'react';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    scrollIntoView: () => {
      inputRef.current.scrollIntoView();
    },
    getValue: () => {
      return inputRef.current.value;
    }
  }));

  return <input ref={inputRef} {...props} />;
});

function Parent() {
  const fancyInputRef = useRef();
  
  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button onClick={() => fancyInputRef.current.focus()}>
        Focus input
      </button>
    </div>
  );
}

React 18+ Concurrent Hooks

React 18 introduced several new hooks that work with concurrent features. These are essentails functionlities when we are creating complex but smooth and responsive user interfaces.

useId: Unique IDs for Accessibility

Generates unique IDs that are stable across server and client rendering.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React, { useId } from 'react';

function FormField({ label, children }) {
  const id = useId();
  
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <div id={id}>
        {children}
      </div>
    </div>
  );
}

function LoginForm() {
  const passwordHintId = useId();
  
  return (
    <form>
      <FormField label="Username">
        <input name="username" />
      </FormField>
      
      <FormField label="Password">
        <input 
          name="password" 
          type="password"
          aria-describedby={passwordHintId}
        />
        <div id={passwordHintId}>
          Password must be at least 8 characters
        </div>
      </FormField>
    </form>
  );
}

useTransition: Non-Blocking State Updates

Mark state updates as non-urgent to keep the UI responsive.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React, { useState, useTransition, useDeferredValue } from 'react';

function SearchResults({ query }) {
  // Expensive component that renders many results
  const results = expensiveSearch(query);
  
  return (
    <div>
      {results.map(result => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  );
}

function SearchApp() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const deferredQuery = useDeferredValue(query);
  
  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value); // Urgent update for input
    
    // Non-urgent update for search results
    startTransition(() => {
      setSearchQuery(value);
    });
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="Search..."
      />
      
      {isPending && <div>Searching...</div>}
      
      <SearchResults query={deferredQuery} />
    </div>
  );
}

useSyncExternalStore: Subscribing to External Stores

Safely subscribe to external data sources in concurrent React.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { useSyncExternalStore } from 'react';

// External store (could be Redux, Zustand, etc.)
class WindowSizeStore {
  constructor() {
    this.listeners = new Set();
    this.width = window.innerWidth;
    this.height = window.innerHeight;
    
    window.addEventListener('resize', this.handleResize);
  }
  
  handleResize = () => {
    this.width = window.innerWidth;
    this.height = window.innerHeight;
    this.listeners.forEach(listener => listener());
  };
  
  subscribe = (listener) => {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  };
  
  getSnapshot = () => ({
    width: this.width,
    height: this.height
  });
}

const windowSizeStore = new WindowSizeStore();

function useWindowSize() {
  return useSyncExternalStore(
    windowSizeStore.subscribe,
    windowSizeStore.getSnapshot,
    () => ({ width: 1024, height: 768 }) // Server snapshot
  );
}

function WindowInfo() {
  const { width, height } = useWindowSize();
  
  return (
    <div>
      Window size: {width} x {height}
    </div>
  );
}

Conclusion

React Hooks have fundamentally changed how we build React applications, providing a more intuitive and powerful way to manage state, side effects, and component logic. With React 18’s concurrent features and the growing ecosystem of hook-based libraries, mastering hooks is essential for modern React development.

Key takeaways for working with hooks effectively:

  1. Follow the Rules: Always call hooks at the top level and only from React functions
  2. Understand Dependencies: Properly manage dependency arrays to avoid bugs and performance issues
  3. Custom Hooks: Extract reusable logic into custom hooks for better code organization
  4. Performance: Use optimization hooks judiciously - measure first, optimize when needed
  5. Testing: Test your hooks thoroughly, focusing on behavior rather than implementation
  6. Stay Updated: Keep up with new hooks and features as React continues to evolve

The hooks ecosystem continues to grow and mature, with new patterns and best practices emerging regularly. By understanding these fundamentals and staying current with the latest developments, you’ll be well-equipped to build robust, performant React applications that take full advantage of what hooks have to offer.

Remember: hooks are just functions, but they unlock the power to make your React components more modular, reusable, and easier to reason about. The key is to use them thoughtfully and follow established patterns while staying open to new possibilities as the ecosystem evolves.

This post is licensed under CC BY 4.0 by the author.