How do you use refs for DOM interaction in React? What are the different types of refs?

5 minintermediatereactrefsdominteraction

Quick Answer

Refs provide a way to access DOM elements or component instances directly. They're essential for DOM manipulation, focus management, and integrating with third-party libraries.

Detailed Answer

How do you use refs for DOM interaction in React? What are the different types of refs?

Answer:

Refs provide a way to access DOM elements or component instances directly. They're essential for DOM manipulation, focus management, and integrating with third-party libraries.

Types of Refs:

1. useRef Hook (Most Common):

function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [value, setValue] = useState('');

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

  const selectAll = () => {
    inputRef.current?.select();
  };

  return (
    <div>
      <input
        ref={inputRef}
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Type something..."
      />
      <button onClick={focusInput}>Focus Input</button>
      <button onClick={selectAll}>Select All</button>
    </div>
  );
}

2. Callback Refs:

function CallbackRefExample() {
  const [inputElement, setInputElement] = useState<HTMLInputElement | null>(null);

  const inputRef = useCallback((node: HTMLInputElement | null) => {
    if (node) {
      setInputElement(node);
      node.focus(); // Auto-focus when element is created
    }
  }, []);

  const measureElement = () => {
    if (inputElement) {
      const rect = inputElement.getBoundingClientRect();
      console.log('Input dimensions:', rect);
    }
  };

  return (
    <div>
      <input ref={inputRef} placeholder="Auto-focused input" />
      <button onClick={measureElement}>Measure Input</button>
    </div>
  );
}

3. String Refs (Legacy - Don't Use):

// DON'T USE - Legacy pattern
class LegacyComponent extends Component {
  render() {
    return <input ref="myInput" />;
  }
  
  componentDidMount() {
    this.refs.myInput.focus(); // Deprecated
  }
}

DOM Manipulation Examples:

Measuring Elements:

function MeasurableComponent() {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  const elementRef = useRef<HTMLDivElement>(null);

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

  useEffect(() => {
    measureElement();
    window.addEventListener('resize', measureElement);
    
    return () => window.removeEventListener('resize', measureElement);
  }, [measureElement]);

  return (
    <div>
      <div
        ref={elementRef}
        style={{
          width: '200px',
          height: '100px',
          background: 'lightblue',
          margin: '20px'
        }}
      >
        Measurable content
      </div>
      <p>Width: {dimensions.width}px, Height: {dimensions.height}px</p>
      <button onClick={measureElement}>Re-measure</button>
    </div>
  );
}

Scroll Management:

function ScrollableList() {
  const listRef = useRef<HTMLDivElement>(null);
  const [items] = useState(Array.from({ length: 100 }, (_, i) => `Item ${i + 1}`));

  const scrollToTop = () => {
    listRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
  };

  const scrollToBottom = () => {
    if (listRef.current) {
      listRef.current.scrollTo({
        top: listRef.current.scrollHeight,
        behavior: 'smooth'
      });
    }
  };

  const scrollToItem = (index: number) => {
    const itemElement = listRef.current?.children[index] as HTMLElement;
    if (itemElement) {
      itemElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
  };

  return (
    <div>
      <div style={{ marginBottom: '10px' }}>
        <button onClick={scrollToTop}>Scroll to Top</button>
        <button onClick={scrollToBottom}>Scroll to Bottom</button>
        <button onClick={() => scrollToItem(50)}>Scroll to Item 50</button>
      </div>
      
      <div
        ref={listRef}
        style={{
          height: '300px',
          overflow: 'auto',
          border: '1px solid #ccc',
          padding: '10px'
        }}
      >
        {items.map((item, index) => (
          <div key={index} style={{ padding: '5px', borderBottom: '1px solid #eee' }}>
            {item}
          </div>
        ))}
      </div>
    </div>
  );
}

Focus Management:

function FocusManager() {
  const firstInputRef = useRef<HTMLInputElement>(null);
  const secondInputRef = useRef<HTMLInputElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);

  const handleKeyDown = (e: React.KeyboardEvent, nextRef: React.RefObject<HTMLElement>) => {
    if (e.key === 'Enter' || e.key === 'Tab') {
      e.preventDefault();
      nextRef.current?.focus();
    }
  };

  return (
    <div>
      <input
        ref={firstInputRef}
        placeholder="First input (press Enter to go to next)"
        onKeyDown={(e) => handleKeyDown(e, secondInputRef)}
      />
      <input
        ref={secondInputRef}
        placeholder="Second input (press Enter to go to button)"
        onKeyDown={(e) => handleKeyDown(e, buttonRef)}
      />
      <button
        ref={buttonRef}
        onClick={() => firstInputRef.current?.focus()}
      >
        Back to First Input
      </button>
    </div>
  );
}

Custom Hook for DOM Measurements:

function useElementSize<T extends HTMLElement = HTMLDivElement>() {
  const ref = useRef<T>(null);
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const resizeObserver = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const { width, height } = entry.contentRect;
        setSize({ width, height });
      }
    });

    resizeObserver.observe(element);

    return () => {
      resizeObserver.disconnect();
    };
  }, []);

  return { ref, size };
}

// Usage
function ResizableComponent() {
  const { ref, size } = useElementSize<HTMLDivElement>();

  return (
    <div>
      <div
        ref={ref}
        style={{
          width: '100%',
          minHeight: '100px',
          background: 'lightgreen',
          padding: '20px'
        }}
      >
        This element is {size.width}px wide and {size.height}px tall
      </div>
    </div>
  );
}

Refs with Third-party Libraries:

function ChartComponent() {
  const chartRef = useRef<HTMLCanvasElement>(null);
  const chartInstance = useRef<Chart | null>(null);

  useEffect(() => {
    if (chartRef.current && !chartInstance.current) {
      // Initialize chart library
      chartInstance.current = new Chart(chartRef.current, {
        type: 'line',
        data: {
          labels: ['Jan', 'Feb', 'Mar', 'Apr'],
          datasets: [{
            label: 'Sales',
            data: [12, 19, 3, 5],
            borderColor: 'rgb(75, 192, 192)',
          }]
        }
      });
    }

    return () => {
      if (chartInstance.current) {
        chartInstance.current.destroy();
        chartInstance.current = null;
      }
    };
  }, []);

  const updateChart = (newData: number[]) => {
    if (chartInstance.current) {
      chartInstance.current.data.datasets[0].data = newData;
      chartInstance.current.update();
    }
  };

  return (
    <div>
      <canvas ref={chartRef} width={400} height={200} />
      <button onClick={() => updateChart([1, 2, 3, 4])}>
        Update Chart
      </button>
    </div>
  );
}

Ref Best Practices:

  1. Use useRef for DOM access - Most common pattern
  2. Use callback refs for dynamic refs - When ref target changes
  3. Always check if ref.current exists - Use optional chaining
  4. Clean up third-party library instances - In useEffect cleanup
  5. Don't use refs for data flow - Use state and props instead
  6. Use forwardRef for component refs - When passing refs to child components
// forwardRef example
const FancyInput = forwardRef<HTMLInputElement, { placeholder: string }>(
  ({ placeholder }, ref) => {
    return (
      <input
        ref={ref}
        placeholder={placeholder}
        style={{
          border: '2px solid #007bff',
          borderRadius: '4px',
          padding: '8px'
        }}
      />
    );
  }
);

// Usage
function ParentComponent() {
  const inputRef = useRef<HTMLInputElement>(null);

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

  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Fancy input" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}