Understanding React Hooks: State, Effects, and Performance Optimization

State Snapshot in Event Handlers

In React, useState provides a mechanism to "remember" values across renders, but it does not make state "instant". When you call setIndex inside an event handler, the state update is not applied immediately. Instead, React queues the update and uses the current snapshot of state for the current render.

For example, when a click handler runs:

  1. setIndex schedules a new render with the updated index.
  2. The original index value (as of that render) is logged.
  3. React re-renders the component, and the new index is used in the next render.

This behavior can be imagined as React storing a temporary copy (tempIndex = index) inside the function scope, and the console.log refers to that copy.

Re-rendering Requires setState

Unlike Vue, React is not reactive. In the example below, if you modify the object directly without calling setObj, the component will not re-render. React intentionally avoids proxying state changes.

const [obj, setObj] = useState({ num: 1 });
obj.num = 5; // does NOT trigger re-render

To trigger a re-render, you must call the setter:

setObj({ num: 5 });

Modifying Objects and Arrays

Becuase React is not reactive, you must replace the entire object or array to trigger updates. However, libraries like use-immer can simplify mutations using drafts:

import { useImmer } from 'use-immer';

function List() {
  const [obj, setObj] = useImmer({ count: 1 });
  const [items, setItems] = useImmer([]);

  const handleClick = () => {
    setItems(draft => draft.push(3));
    setObj(draft => { draft.count = 5; });
  };

  return <h2 onClick={handleClick}>{items.length}</h2>;
}

Under the hood, useImmer creates a copy of the draft object, and after modifications, it replaces the entire state.

State Setting Principles

In Vue, data is reactive, but in React you must manually call setters. Computed values in React are simply expressions evaluated on every render (because the component re-runs). For example:

const [count, setCount] = useState(0);
const doubled = count * 2; // recomputed every render

Component Rendering and State Preservation

React builds a UI tree. If the same component type occupies the same position across renders, its state is preserved. If the position changes (e.g., conditional rendering), state resets.

Example 1 – State reset due to position change:

{show && <Counter />}
{!show && <Counter />}

Here, two different Counter components are rendered in different positions, so their states are independent.

Example 2 – State preserved same component same position:

if (isFancy) {
  return <div><Counter fancy /></div>;
}
return <div><Counter /></div>;

Even though the props differ, the Counter sits in the same location, so its state persists.

Using key to force reset: When you want two different states for the same component type, assign unique keys.

{show ? <Counter key="A" /> : <Counter key="B" />}

useReducer

useReducer is a syntactic sugar for state management that follows the reducer pattern. It must always return a new state and should not contain side effects like async operations. Example:

function reducer(state, action) { switch (action.type) { case 'send': return { ...state, message: '' }; default: return state; } }

<p>Note: Side effects (like alerts) should be placed outside the reducer, typically in event handlers, to avoid infinite loops.</p>

<h2><code>useContext</code> for Prop Drilling</h2>
<p><code>useContext</code> provides a way to pass data through the component tree without prop drilling. Steps:</p>
<ol>
<li>Create a context: <code>const MyContext = createContext(defaultValue);</code></li>
<li>Provide the context at a higher level: <code><MyContext.Provider value={...}></code></li>
<li>Consume it in any descendant: <code>const value = useContext(MyContext);</code></li>
</ol>
<p>When multiple providers exist, the nearest one takes precedence. For sibling communication, lift state up to a common ancestor.</p>

<h2><code>useRef</code> for Mutable Values and DOM Access</h2>
<p><code>useRef</code> holds a value that persists across renders without causing re-renders when changed. It is commonly used to access DOM elements or store mutable data that must be fresh across renders.</p>
<p>Example – preserving the latest value for an async operation:</p>
<code>const latestMessage = useRef('');
const handleSend = () => {
  setTimeout(() => {
    alert(latestMessage.current); // uses the most recent value
  }, 3000);
};
</code>
<p>Without <code>useRef</code>, the snapshot of <code>message</code> at the time of the click would be captured.</p>

<h2>Exposing Refs via <code>forwardRef</code></h2>
<p>To let a parent component access a child component's DOM node, wrap the child with <code>forwardRef</code> and forward the ref to the desired element:</p>
<code>const SearchInput = forwardRef((props, ref) => (
  <input ref={ref} {...props} />
));
</code>

<h2>Imperative Methods with <code>useImperativeHandle</code></h2>
<p>Combine <code>useImperativeHandle</code> and <code>forwardRef</code> to expose custom methods or properties:</p>
<code>const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  useImperativeHandle(ref, () => ({
    reset() { setCount(0); }
  }));
  return <div>{count}</div>;
});
</code>
<p>Parent can then call <code>childRef.current.reset()</code>. Use this sparingly.</p>

<h2><code>useEffect</code> – Side Effects after Render</h2>
<p><code>useEffect</code> runs after the component renders. It depends on changes to the dependency array, but the effect only runs if the component function actually re-executes due to a state change.</p>
<p>Important: Changing a <code>useRef</code> value does not cause re-renders, so an effect dependent on <code>ref.current</code> will not fire unless another state change triggers a re-render.</p>
<p><strong>Empty dependency array (<code>[]</code>):</strong> The effect runs only once after the initial render. Useful for one-time initializations like auto-focus.</p>
<code>useEffect(() => {
  inputRef.current.focus();
}, []);
</code>
<p><strong>Cleanup function:</strong> Return a function to clean up resources (e.g., timers, subscriptions) to prevent memory leaks or race conditions.</p>
<code>useEffect(() => {
  const timer = setInterval(() => console.log('tick'), 1000);
  return () => clearInterval(timer);
}, []);
</code>

<h2><code>useEffectEvent</code> (Experimental)</h2>
<p>This hook extracts non‑reactive logic from effects. For example, you might want to reconnect a chat only when the room changes, but still display the latest theme in the notification:</p>
<code>const onConnected = useEffectEvent((theme) => {
  showNotification('Connected', theme);
});

useEffect(() => {
  const connection = createConnection(roomId);
  connection.on('connected', () => onConnected(theme));
  return () => connection.disconnect();
}, [roomId]);
</code>

<h2><code>useLayoutEffect</code> – Synchronous Layout Measurement</h2>
<p><code>useLayoutEffect</code> fires synchronously after DOM mutations, before the browser paints. It is useful for measuring layout and making visual adjustments without a flash.</p>
<code>useLayoutEffect(() => {
  const height = divRef.current.getBoundingClientRect().height;
  // Adjust tooltip position
}, [value]);
</code>
<p>Use <code>useLayoutEffect</code> when you need to read layout and update the UI in the same render cycle; otherwise prefer <code>useEffect</code> for performance.</p>

<h2>Caching Functions with <code>useCallback</code></h2>
<p><code>useCallback</code> memoizes a function between renders. It is often paired with <code>React.memo</code> to avoid unnecessary re‑renders of child components when the function reference hasn't changed.</p>
<code>const handleSubmit = useCallback((orderDetails) => {
  post('/buy', orderDetails);
}, [productId, referrer]);
</code>
<p>If the dependencies remain the same, the function reference stays stable. When using state inside the callback, prefer the updater form of the setter to avoid adding state as a dependency:</p>
<code>const handleAdd = useCallback((text) => {
  setTodos(prev => [...prev, { id: nextId++, text }]);
}, []);
</code>

<h2>Caching Values with <code>useMemo</code></h2>
<p><code>useMemo</code> memoizes the result of a computation. It only recomputes when dependencies change. Useful for expensive calculations:</p>
<code>const filteredTodos = useMemo(() => {
  return todos.filter(todo => todo.text.includes(searchTerm));
}, [todos, searchTerm]);
</code>
<p>Note: Do not rely on <code>useMemo</code> for side effects; use <code>useEffect</code> instead.</p>

<h2>Custom Hooks</h2>
<p>Custom hooks let you extract reusable stateful logic. They are just functions that can call other hooks. For example, a hook to track window width:</p>
<code>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;
}
</code>

Tags: React Hooks useState useEffect useRef

Posted on Wed, 10 Jun 2026 17:59:08 +0000 by b