Frontend / React / use_ref.md

useRef Hook in React

Updated 4 interview angles 11 min read source
On this page9
  1. Table of Contents
  2. What is useRef?
  3. Basic Usage
  4. Common Use Cases
  5. Advanced Patterns
  6. Best Practices
  7. Common Interview Questions
  8. Summary
  9. Interview angle

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:

javascript
// 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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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 4

  • “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.