Back to articles
Technology Insight

React Hooks Deep Dive: useState, useEffect, useContext, useRef, and Custom Hooks

April 13, 2026

React Hooks

The New Era of Functional Components

React Hooks were first introduced in version 16.8 and have revolutionized the React ecosystem. Previously, essential features like State or Lifecycle methods were exclusive to Class Components. This often led to verbose code, difficulty in reusing logic, and the dreaded "wrapper hell".

With Hooks, you can leverage the full power of React inside Functional Components. This article dives deep into the most critical hooks that every React developer must master.

1. useState: Managing State

This is the most basic hook. It allows a functional component to "remember" information between renders.

Syntax: const [state, setState] = useState(initialValue);

Unlike this.setState in Class Components (which merges objects), the useState updater function overwrites the old value completely. Let's look at an example of managing an object:

// WRONG: Old information is lost if not copied
setUser({ name: 'Tuan' }); 

// RIGHT: Use Spread Operator to preserve old fields
const [user, setUser] = useState({ name: 'Nam', age: 25 });

const updateName = () => {
    setUser(prevUser => ({
        ...prevUser, // Copy old data
        name: 'Tuan' // Update new data
    }));
};

2. useEffect: Handling Side Effects

useEffect is a "super hook" that replaces three lifecycle methods: componentDidMount, componentDidUpdate, and componentWillUnmount.

It is used for tasks like: fetching APIs, subscribing to event listeners, or manually changing the DOM.

Three Common Use Cases

  • 1. No Dependency Array:

    Runs after EVERY render. Rarely used as it can cause infinite loops.

    useEffect(() => { ... })
  • 2. Empty Dependency Array ([]):

    Runs only ONCE after mount (equivalent to componentDidMount).

    useEffect(() => { ... }, [])
  • 3. With Dependencies ([prop, state]):

    Runs on mount AND whenever any value in the array changes.

    useEffect(() => { ... }, [userId])

Cleanup Function: To avoid memory leaks (e.g., when using setInterval or addEventListener), you need to return a cleanup function:

useEffect(() => {
    const handleResize = () => console.log(window.innerWidth);
    window.addEventListener('resize', handleResize);

    // This function runs when the component unmounts
    return () => {
        window.removeEventListener('resize', handleResize);
    };
}, []);

3. useContext: Avoiding Prop Drilling

When you need to pass data through many component layers (e.g., Theme, User Auth, Language), manual prop passing (Prop Drilling) becomes cumbersome. useContext allows you to access values from a Context Provider anywhere in the component tree.

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  // No need to receive props, get directly from Context
  const theme = useContext(ThemeContext);
  return <div className={theme}>Toolbar Styled</div>;
}

4. useRef: Mutable References

useRef is commonly known for accessing DOM elements directly, but it has another critical use: storing mutable values without triggering re-renders.

Accessing DOM

const inputRef = useRef(null);

const focusInput = () => {
  inputRef.current.focus();
};

return <input ref={inputRef} />;

Storing Variables

const count = useRef(0);

// This change DOES NOT 
// cause the component to re-render
count.current++; 

5. Custom Hooks: Reusing Logic

This is the most powerful feature. If you find yourself copy-pasting logic (e.g., fetching data, form handling) between components, extract it into a Custom Hook.

The only rule: The function name must start with use.

// useWindowWidth.js
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return width;
}

// Usage in Component
function MyComponent() {
  const width = useWindowWidth();
  return <span>Window width is: {width}</span>;
}

The Two Golden Rules of Hooks

  1. Only Call Hooks at the Top Level: Never call hooks inside loops, conditions, or nested functions. React relies on the call order to manage state.
  2. Only Call Hooks from React Functions: Only call them from React Functional Components or Custom Hooks.

Conclusion

React Hooks are not just a syntax change; they represent a shift in programming mindset (Mental Model). Moving from "Lifecycle" thinking to "Synchronization" (synchronizing data).

By mastering useState, useEffect, and the ability to create Custom Hooks, you will write React applications that are cleaner, easier to maintain, and more performant.