Skip to main content
useRef is a React Hook that lets you reference a value that’s not needed for rendering.
function useRef<T>(initialValue: T): { current: T }

Parameters

initialValue
T
required
The value you want the ref object’s current property to be initially. It can be a value of any type. This argument is ignored after the initial render.

Returns

useRef returns an object with a single property:
current
T
Initially set to the initialValue you passed. You can later set it to something else. If you pass the ref object to React as a ref attribute to a JSX node, React will set its current property.On subsequent renders, useRef will return the same object.
Changing the ref.current property does not trigger a re-render. React is not aware of when you change it because a ref is a plain JavaScript object.

Usage

Referencing a value with a ref

Call useRef at the top level of your component to declare a ref:
import { useRef } from 'react';

function Stopwatch() {
  const intervalRef = useRef(0);
  // intervalRef.current can be used to store any value
  
  function handleStart() {
    const intervalId = setInterval(() => {
      // Update time
    }, 1000);
    intervalRef.current = intervalId;
  }
  
  function handleStop() {
    clearInterval(intervalRef.current);
  }
  
  return (
    <>
      <button onClick={handleStart}>Start</button>
      <button onClick={handleStop}>Stop</button>
    </>
  );
}

Manipulating the DOM with a ref

It’s particularly common to use a ref to manipulate the DOM:
import { useRef } from 'react';

function Form() {
  const inputRef = useRef(null);
  
  function handleClick() {
    inputRef.current.focus();
  }
  
  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>Focus the input</button>
    </>
  );
}
function AutoFocusInput() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} />;
}

Avoiding recreating the ref contents

React saves the initial ref value once and ignores it on subsequent renders:
function VideoPlayer() {
  // ❌ new Video() is called on every render
  const playerRef = useRef(new VideoPlayer());
  
  // ✅ Only called if current is null
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
  
  // ✅ Or use a function (not called on re-renders)
  function getPlayer() {
    if (playerRef.current === null) {
      playerRef.current = new VideoPlayer();
    }
    return playerRef.current;
  }
}

Common Use Cases

Storing timeout/interval IDs

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);
  
  function startTimer() {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  }
  
  function stopTimer() {
    clearInterval(intervalRef.current);
  }
  
  useEffect(() => {
    return () => {
      // Cleanup on unmount
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

Tracking previous value

function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  
  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Storing mutable values

function Form() {
  const [email, setEmail] = useState('');
  const didMount = useRef(false);
  
  useEffect(() => {
    if (!didMount.current) {
      didMount.current = true;
      return; // Skip first render
    }
    
    // This runs on updates but not on mount
    validateEmail(email);
  }, [email]);
  
  return <input value={email} onChange={e => setEmail(e.target.value)} />;
}

Measuring DOM elements

function MeasureElement() {
  const ref = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  
  useEffect(() => {
    if (ref.current) {
      const { width, height } = ref.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);
  
  return (
    <div ref={ref}>
      Width: {dimensions.width}, Height: {dimensions.height}
    </div>
  );
}

Accessing latest props in callbacks

function Chat({ onMessage }) {
  const [messages, setMessages] = useState([]);
  const onMessageRef = useRef(onMessage);
  
  // Keep ref in sync with latest callback
  useEffect(() => {
    onMessageRef.current = onMessage;
  }, [onMessage]);
  
  useEffect(() => {
    const connection = createConnection();
    connection.on('message', (msg) => {
      // Always uses latest onMessage
      onMessageRef.current(msg);
    });
    return () => connection.disconnect();
  }, []); // Empty deps - connection never recreated
  
  return <div>{/* ... */}</div>;
}

TypeScript

import { useRef } from 'react';

// DOM element refs
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);

// Check if current exists before using
function focusInput() {
  if (inputRef.current) {
    inputRef.current.focus();
  }
}

// Or use non-null assertion if you know it's defined
function focusInput() {
  inputRef.current!.focus();
}

// Mutable value refs
const countRef = useRef<number>(0);
const timerRef = useRef<number | null>(null);

interface Player {
  play(): void;
  pause(): void;
}

const playerRef = useRef<Player | null>(null);

Ref with initial value

// Ref that's never null
const idRef = useRef<number>(1);
idRef.current = 2; // OK

// Ref that can be null
const elementRef = useRef<HTMLDivElement | null>(null);

Custom ref types

interface CustomInput {
  focus: () => void;
  reset: () => void;
}

const customRef = useRef<CustomInput>(null);

// Usage
if (customRef.current) {
  customRef.current.focus();
  customRef.current.reset();
}

Troubleshooting

I can’t get a ref to a custom component

By default, you can’t pass refs to custom components:
function MyInput() {
  return <input />;
}

// ❌ This doesn't work
function Parent() {
  const ref = useRef(null);
  return <MyInput ref={ref} />; // Error!
}
Use forwardRef to enable refs:
import { forwardRef } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
  return <input ref={ref} />;
});

// ✅ Now this works
function Parent() {
  const ref = useRef(null);
  return <MyInput ref={ref} />;
}

My ref.current is null

This happens before the component mounts:
function Component() {
  const ref = useRef(null);
  
  // ❌ ref.current is null here during first render
  console.log(ref.current); // null
  
  useEffect(() => {
    // ✅ ref.current is set after mount
    console.log(ref.current); // <div>...</div>
  }, []);
  
  return <div ref={ref}>Content</div>;
}

Changing ref.current doesn’t re-render

This is expected behavior. If you need re-renders, use useState instead:
// ❌ Won't re-render when ref changes
function Component() {
  const ref = useRef(0);
  
  function handleClick() {
    ref.current += 1;
    // Component doesn't re-render!
  }
}

// ✅ Will re-render when state changes
function Component() {
  const [count, setCount] = useState(0);
  
  function handleClick() {
    setCount(count + 1);
    // Component re-renders
  }
}

Should I use ref or state?

Use useState when:
  • The value is displayed in the UI
  • Changes should trigger re-renders
  • You need React to “react” to changes
Use useRef when:
  • Storing values that don’t affect rendering
  • Accessing DOM elements
  • Keeping mutable values across renders
  • Storing timeout/interval IDs

ref vs state

Featurerefstate
Triggers re-renderNoYes
MutableYes (mutate .current directly)No (use setter function)
Value during renderCurrent valueValue at render time
Use caseSide effects, DOM accessUI data
function Example() {
  // State: Affects rendering
  const [count, setCount] = useState(0);
  
  // Ref: Doesn't affect rendering
  const renderCount = useRef(0);
  
  useEffect(() => {
    renderCount.current += 1;
  });
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Renders: {renderCount.current}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}