frontend / react / use_ref.md

useRef Hook in React

4 interview angles 11 min read source

useRef Hook in React

The useRef hook is a powerful React hook that allows you to persist values between renders without causing re-renders and provides a way to access DOM elements directly.

Table of Contents


What is useRef?

useRef is a React hook that returns a mutable ref object with a .current property that can be used to store values that persist across renders without causing re-renders.

Key Concepts:

  • Mutable Reference: The .current property can be changed without triggering re-renders
  • Persistent Values: Values stored in refs persist between component renders
  • DOM Access: Can be used to directly access DOM elements
  • Performance: Useful for storing values that don’t need to trigger re-renders

useRef vs useState:

// useState: Triggers re-render when value changes
function StateExample() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1); // This triggers a re-render
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

// useRef: No re-render when value changes
function RefExample() {
  const countRef = useRef(0);

  const increment = () => {
    countRef.current += 1; // This does NOT trigger a re-render
    console.log('Count:', countRef.current);
  };

  return (
    <div>
      <p>Count: {countRef.current}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

Basic Usage

1. Creating and Using Refs:

import React, { useRef, useEffect } from 'react';

function BasicRefExample() {
  // Create a ref with initial value
  const countRef = useRef(0);
  const inputRef = useRef(null);
  const previousValueRef = useRef(null);

  // Access and modify ref values
  const handleClick = () => {
    countRef.current += 1;
    console.log('Current count:', countRef.current);
  };

  // Focus input on mount
  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  // Store previous value
  const handleInputChange = (e) => {
    previousValueRef.current = e.target.value;
  };

  return (
    <div>
      <h3>Basic Ref Example</h3>

      <div>
        <button onClick={handleClick}>
          Increment Count (Check console)
        </button>
        <p>Count: {countRef.current}</p>
      </div>

      <div>
        <input
          ref={inputRef}
          onChange={handleInputChange}
          placeholder="This will be focused on mount"
        />
      </div>

      <div>
        <p>Previous value: {previousValueRef.current}</p>
      </div>
    </div>
  );
}

2. DOM Element Access:

function DOMAccessExample() {
  const buttonRef = useRef(null);
  const divRef = useRef(null);

  const handleButtonClick = () => {
    // Access DOM element properties
    if (buttonRef.current) {
      console.log('Button text:', buttonRef.current.textContent);
      console.log('Button position:', buttonRef.current.getBoundingClientRect());

      // Modify DOM element
      buttonRef.current.style.backgroundColor = 'red';
    }
  };

  const handleDivClick = () => {
    if (divRef.current) {
      // Scroll to element
      divRef.current.scrollIntoView({ behavior: 'smooth' });

      // Get computed styles
      const styles = window.getComputedStyle(divRef.current);
      console.log('Background color:', styles.backgroundColor);
    }
  };

  return (
    <div>
      <h3>DOM Access Example</h3>

      <button ref={buttonRef} onClick={handleButtonClick}>
        Click me to see DOM info
      </button>

      <div
        ref={divRef}
        onClick={handleDivClick}
        style={{
          width: '200px',
          height: '100px',
          backgroundColor: 'lightblue',
          margin: '20px 0',
          cursor: 'pointer'
        }}
      >
        Click to scroll to this div
      </div>

      <div style={{ height: '500px', backgroundColor: 'lightgray' }}>
        Scroll down to see the div above
      </div>
    </div>
  );
}

3. Storing Previous Values:

function PreviousValueExample() {
  const [count, setCount] = useState(0);
  const previousCountRef = useRef();

  useEffect(() => {
    // Store previous value before updating
    previousCountRef.current = count;
  });

  return (
    <div>
      <h3>Previous Value Example</h3>

      <p>Current count: {count}</p>
      <p>Previous count: {previousCountRef.current}</p>

      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>

      <button onClick={() => setCount(count - 1)}>
        Decrement
      </button>
    </div>
  );
}

Common Use Cases

1. Focus Management:

function FocusManagementExample() {
  const inputRef = useRef(null);
  const buttonRef = useRef(null);

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

  const focusButton = () => {
    buttonRef.current?.focus();
  };

  const selectInputText = () => {
    if (inputRef.current) {
      inputRef.current.select();
    }
  };

  return (
    <div>
      <h3>Focus Management</h3>

      <input
        ref={inputRef}
        type="text"
        placeholder="Type something..."
        defaultValue="Sample text"
      />

      <button ref={buttonRef}>
        Button
      </button>

      <div style={{ marginTop: '10px' }}>
        <button onClick={focusInput}>Focus Input</button>
        <button onClick={focusButton}>Focus Button</button>
        <button onClick={selectInputText}>Select Input Text</button>
      </div>
    </div>
  );
}

2. Timer Management:

function TimerExample() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  const startTimer = () => {
    if (intervalRef.current) return; // Prevent multiple timers

    intervalRef.current = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
  };

  const stopTimer = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };

  const resetTimer = () => {
    stopTimer();
    setCount(0);
  };

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  return (
    <div>
      <h3>Timer Example</h3>

      <p>Count: {count}</p>

      <button onClick={startTimer}>Start Timer</button>
      <button onClick={stopTimer}>Stop Timer</button>
      <button onClick={resetTimer}>Reset Timer</button>
    </div>
  );
}

3. Form Validation:

function FormValidationExample() {
  const [errors, setErrors] = useState({});
  const formRef = useRef(null);

  const validateForm = () => {
    const formData = new FormData(formRef.current);
    const newErrors = {};

    // Validate required fields
    if (!formData.get('name')) {
      newErrors.name = 'Name is required';
    }

    if (!formData.get('email')) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(formData.get('email'))) {
      newErrors.email = 'Invalid email format';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    if (validateForm()) {
      console.log('Form is valid, submitting...');
      // Submit form logic here
    } else {
      console.log('Form has errors');
    }
  };

  return (
    <div>
      <h3>Form Validation Example</h3>

      <form ref={formRef} onSubmit={handleSubmit}>
        <div>
          <input
            name="name"
            placeholder="Name"
            style={{ border: errors.name ? '1px solid red' : '1px solid black' }}
          />
          {errors.name && <span style={{ color: 'red' }}>{errors.name}</span>}
        </div>

        <div>
          <input
            name="email"
            type="email"
            placeholder="Email"
            style={{ border: errors.email ? '1px solid red' : '1px solid black' }}
          />
          {errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
        </div>

        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

4. Animation Control:

function AnimationExample() {
  const elementRef = useRef(null);
  const animationRef = useRef(null);

  const startAnimation = () => {
    if (elementRef.current) {
      elementRef.current.style.transition = 'all 0.5s ease';
      elementRef.current.style.transform = 'translateX(200px) rotate(360deg)';
    }
  };

  const stopAnimation = () => {
    if (elementRef.current) {
      elementRef.current.style.transition = 'none';
      elementRef.current.style.transform = 'translateX(0) rotate(0deg)';
    }
  };

  const pulseAnimation = () => {
    if (elementRef.current) {
      elementRef.current.style.animation = 'pulse 1s infinite';
    }
  };

  return (
    <div>
      <h3>Animation Example</h3>

      <div
        ref={elementRef}
        style={{
          width: '100px',
          height: '100px',
          backgroundColor: 'blue',
          margin: '20px 0'
        }}
      />

      <button onClick={startAnimation}>Start Animation</button>
      <button onClick={stopAnimation}>Stop Animation</button>
      <button onClick={pulseAnimation}>Pulse Animation</button>

      <style>
        {`
          @keyframes pulse {
            0% { transform: scale(1); }
            50% { transform: scale(1.2); }
            100% { transform: scale(1); }
          }
        `}
      </style>
    </div>
  );
}

Advanced Patterns

1. Custom Hook for Previous Value:

function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
}

// Usage
function PreviousValueHook() {
  const [count, setCount] = useState(0);
  const previousCount = usePrevious(count);

  return (
    <div>
      <h3>Previous Value Hook</h3>
      <p>Current: {count}</p>
      <p>Previous: {previousCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

2. useRef with useImperativeHandle:

const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    blur: () => inputRef.current?.blur(),
    select: () => inputRef.current?.select(),
    getValue: () => inputRef.current?.value,
    setValue: (value) => {
      if (inputRef.current) {
        inputRef.current.value = value;
      }
    }
  }));

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

// Usage
function ImperativeHandleExample() {
  const inputRef = useRef(null);

  const handleFocus = () => inputRef.current?.focus();
  const handleBlur = () => inputRef.current?.blur();
  const handleSelect = () => inputRef.current?.select();
  const handleGetValue = () => console.log('Value:', inputRef.current?.getValue());
  const handleSetValue = () => inputRef.current?.setValue('New value');

  return (
    <div>
      <h3>Imperative Handle Example</h3>

      <CustomInput ref={inputRef} placeholder="Custom input" />

      <div style={{ marginTop: '10px' }}>
        <button onClick={handleFocus}>Focus</button>
        <button onClick={handleBlur}>Blur</button>
        <button onClick={handleSelect}>Select</button>
        <button onClick={handleGetValue}>Get Value</button>
        <button onClick={handleSetValue}>Set Value</button>
      </div>
    </div>
  );
}

3. useRef for Callback Functions:

function CallbackRefExample() {
  const callbackRef = useRef();

  const handleClick = () => {
    if (callbackRef.current) {
      callbackRef.current();
    }
  };

  const setCallback = (callback) => {
    callbackRef.current = callback;
  };

  return (
    <div>
      <h3>Callback Ref Example</h3>

      <button onClick={handleClick}>Execute Callback</button>

      <button onClick={() => setCallback(() => alert('Hello!'))}>
        Set Alert Callback
      </button>

      <button onClick={() => setCallback(() => console.log('Logged!'))}>
        Set Log Callback
      </button>
    </div>
  );
}

4. useRef for Instance Variables:

function InstanceVariableExample() {
  const renderCountRef = useRef(0);
  const lastRenderTimeRef = useRef(Date.now());

  // Track render count
  renderCountRef.current += 1;

  const handleClick = () => {
    const now = Date.now();
    const timeSinceLastRender = now - lastRenderTimeRef.current;

    console.log(`Render #${renderCountRef.current}`);
    console.log(`Time since last render: ${timeSinceLastRender}ms`);

    lastRenderTimeRef.current = now;
  };

  return (
    <div>
      <h3>Instance Variable Example</h3>
      <p>Render count: {renderCountRef.current}</p>
      <button onClick={handleClick}>Click to see render info</button>
    </div>
  );
}

Best Practices

1. Use useRef for Values That Don’t Need Re-renders:

// Good: Use ref for values that don't affect UI
function GoodExample() {
  const timeoutRef = useRef(null);
  const [count, setCount] = useState(0);

  const startTimeout = () => {
    timeoutRef.current = setTimeout(() => {
      console.log('Timeout completed');
    }, 1000);
  };

  const clearTimeout = () => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = null;
    }
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={startTimeout}>Start Timeout</button>
      <button onClick={clearTimeout}>Clear Timeout</button>
    </div>
  );
}

// Bad: Using state for values that don't affect UI
function BadExample() {
  const [timeoutId, setTimeoutId] = useState(null);
  const [count, setCount] = useState(0);

  const startTimeout = () => {
    const id = setTimeout(() => {
      console.log('Timeout completed');
    }, 1000);
    setTimeoutId(id); // Unnecessary re-render
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={startTimeout}>Start Timeout</button>
    </div>
  );
}

2. Clean Up Refs in useEffect:

// Good: Clean up refs properly
function GoodCleanupExample() {
  const intervalRef = useRef(null);

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  return <div>Component with cleanup</div>;
}

// Bad: No cleanup
function BadCleanupExample() {
  const intervalRef = useRef(null);

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);
    // Missing cleanup - memory leak!
  }, []);

  return <div>Component without cleanup</div>;
}

3. Check Ref Existence Before Use:

// Good: Check ref existence
function GoodRefCheck() {
  const inputRef = useRef(null);

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

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={handleFocus}>Focus</button>
    </div>
  );
}

// Bad: No existence check
function BadRefCheck() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current.focus(); // Could throw error if ref is null
  };

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={handleFocus}>Focus</button>
    </div>
  );
}

4. Use Refs for DOM Measurements:

// Good: Use ref for DOM measurements
function GoodDOMExample() {
  const elementRef = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    if (elementRef.current) {
      const rect = elementRef.current.getBoundingClientRect();
      setDimensions({
        width: rect.width,
        height: rect.height
      });
    }
  }, []);

  return (
    <div>
      <div
        ref={elementRef}
        style={{ width: '200px', height: '100px', backgroundColor: 'blue' }}
      />
      <p>Width: {dimensions.width}px</p>
      <p>Height: {dimensions.height}px</p>
    </div>
  );
}

Common Interview Questions

Q: What is useRef and when should you use it?

  • useRef is a hook that returns a mutable ref object used to persist values between renders without causing re-renders.

Q: What’s the difference between useRef and useState?

  • useRef doesn’t trigger re-renders when the value changes, while useState does.

Q: How do you access DOM elements with useRef?

  • Pass the ref to the ref prop of a DOM element, then access it via ref.current.

Q: Can you modify a ref value directly?

  • Yes, you can modify ref.current directly without triggering re-renders.

Q: What are common use cases for useRef?

  • DOM element access, storing previous values, timer management, and form validation.

Q: How do you clean up refs?

  • Use useEffect cleanup functions to clear timers, intervals, or other resources stored in refs.

Q: Can you use useRef with functional components?

  • Yes, useRef works with both functional and class components (via React.createRef).

Q: What happens if you don’t check if a ref exists?

  • You might get runtime errors if trying to access properties of null/undefined.

Q: How do you create a custom hook with useRef?

  • Create a function that starts with “use” and returns a ref or uses refs internally.

Q: When should you avoid using useRef?

  • When the value needs to trigger re-renders or when you need to track state changes.

Summary

  • useRef provides a way to persist values between renders without causing re-renders
  • Common uses include DOM access, timer management, and storing previous values
  • Best practices include proper cleanup, existence checks, and using refs for non-UI values
  • Advanced patterns include custom hooks, imperative handles, and callback refs
  • Performance benefits come from avoiding unnecessary re-renders
  • Understanding useRef is essential for advanced React development and performance optimization

Interview angle

  • “What is useRef actually for?” - a mutable box that persists across renders and does not trigger one when it changes. Two uses: a handle on a DOM node, and instance-like values such as timer IDs, previous values, or a mutable flag.
  • “When does a ref belong instead of state?” - when the value is not rendered. If changing it should update the screen, it is state. Reading a ref during render is also discouraged, because the value is not part of the render’s inputs.
  • “What changed in React 19?” - function components receive ref as an ordinary prop, so forwardRef is no longer needed for new code. Ref callbacks can also return a cleanup function, which replaces the null-check dance.
  • “Why is ref.current null on the first render?” - refs attach after the DOM is created. Read them in an effect or an event handler, not during render.