8 min read

useRef for DOM and mutable state

useRef for DOM and mutable state

Let’s dive into useRef—not as some abstract React hook you memorize, but as your secret weapon for handling real-world chaos like focusing inputs during order entry, tracking scroll positions in long shipment lists, or maintaining mutable values without triggering endless re-renders. By the end, you’ll wield useRef like a pro in order management apps, complete with copy-paste-ready code that runs locally. Think of it as the "behind-the-scenes assistant" that keeps your UI smooth while useState (which we covered in useState practical pitfalls and fixes) handles the visible updates.

If you’ve followed our series—from React vs vanilla JS to JSX under the hoodfirst componentsprops vs statecontrolled inputskeys in listsreusable systemsparent-child comms, and useEffect masteryuseRef is the natural next step for fine-grained control.​

What is useRef? Your Persistent Sidekick

useRef creates a mutable ref object whose .current property persists across re-renders without triggering updates—perfect for order apps where you need to track things like the next shipment ID counter or a focused field during high-volume picking. Unlike useState, changing ref.current doesn’t cause re-renders, making it ideal for internal app state like timers or previous values in fulfillment workflows.

Here’s a complete, runnable App.js to see it in action. Create a new React app (npx create-react-app useref-demo && cd useref-demo), replace src/App.js, and npm start:

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

function OrderCounter() {
  const orderCountRef = useRef(0);
  const [displayCount, setDisplayCount] = useState(0);

  const handleNewOrder = () => {
    orderCountRef.current += 1;  // Mutable, no re-render!
    setDisplayCount(orderCountRef.current);  // Only update display when needed
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'Arial' }}>
      <h2>Order Fulfillment Counter</h2>
      <p>Total Orders Processed: {displayCount}</p>
      <button onClick={handleNewOrder}>
        Process New Shipment
      </button>
      <p>Internal Counter: {orderCountRef.current}</p>
    </div>
  );
}

function App() {
  return <OrderCounter />;
}

export default App;

Click the button—watch how orderCountRef.current increments silently until you sync it to state. This shines in batch picking where you track uncommitted changes before API calls.​

DOM Manipulation: Focus, Measure, Control

In order management, users scan barcodes, jump between fields, or measure container heights for dynamic layouts. useRef attaches to DOM nodes for direct access—focusing the next input after scanning or scrolling to a specific shipment row.

Complete example: Auto-focus next field in a multi-step order entry form. Drop this into App.js:

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

function OrderEntryForm() {
  const customerRef = useRef(null);
  const productRef = useRef(null);
  const quantityRef = useRef(null);
  const stepRef = useRef(0);

  useEffect(() => {
    customerRef.current.focus();
  }, []);

  const handleNextStep = () => {
    stepRef.current += 1;
    if (stepRef.current === 1) productRef.current.focus();
    if (stepRef.current === 2) quantityRef.current.focus();
  };

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') handleNextStep();
  };

  return (
    <div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}>
      <h2>New Order Entry (Tab/Enter to Next)</h2>
      <input
        ref={customerRef}
        placeholder="Customer ID (e.g., CUST-123)"
        onKeyDown={handleKeyPress}
        style={{ display: 'block', margin: '10px 0', padding: '8px', width: '100%' }}
      />
      <input
        ref={productRef}
        placeholder="Product SKU (e.g., PROD-456)"
        onKeyDown={handleKeyPress}
        style={{ display: 'block', margin: '10px 0', padding: '8px', width: '100%' }}
      />
      <input
        ref={quantityRef}
        placeholder="Quantity"
        type="number"
        onKeyDown={handleKeyPress}
        style={{ display: 'block', margin: '10px 0', padding: '8px', width: '100%' }}
      />
      <p>Step: {stepRef.current + 1}/3</p>
    </div>
  );
}

function App() {
  return <OrderEntryForm />;
}

export default App;

Hit Enter after typing—fields auto-focus! This beats manual tabbing in warehouse apps. Pro tip: Combine with useEffect for conditional focus after validation fails.​

Mutable Values: Track Without Re-renders

useRef excels at storing mutable data like previous shipment status (for undo logic) or timers for auto-save in order editing—without the re-render storm useState causes. In backroom pick workflows, track "pick attempts" before committing.

Runnable example: Track order edit history with undo. Full App.js:

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

function OrderEditor() {
  const historyRef = useRef([]);
  const [order, setOrder] = useState({ id: 'ORD-001', status: 'PENDING', items: 5 });

  const updateStatus = (newStatus) => {
    historyRef.current.push({ ...order, timestamp: Date.now() });
    setOrder({ ...order, status: newStatus });
  };

  const undoLastChange = () => {
    if (historyRef.current.length > 0) {
      const previous = historyRef.current.pop();
      setOrder(previous);
    }
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>Order ORD-001 Editor</h2>
      <p>Status: <strong>{order.status}</strong> | Items: {order.items}</p>
      <div>
        <button onClick={() => updateStatus('PACKING')}>Mark Packing</button>
        <button onClick={() => updateStatus('SHIPPED')}>Mark Shipped</button>
        <button onClick={undoLastChange} disabled={historyRef.current.length === 0}>
          Undo Last Change ({historyRef.current.length})
        </button>
      </div>
      <details style={{ marginTop: '20px' }}>
        <summary>History (via ref.current)</summary>
        <pre>{JSON.stringify(historyRef.current.slice(-3), null, 2)}</pre>
      </details>
    </div>
  );
}

function App() {
  return <OrderEditor />;
}

export default App;

Change status, undo—history persists mutably! No re-renders on historyRef.current.push. Ideal for order audit trails without performance hits.​

Previous Value Tracking: Detect Real Changes

Ever needed the "previous prop value" in child components? Like detecting if an order status changed from PENDING to SHIPPED for API calls. useRef stores the last known value perfectly, especially after our parent-child communication deep dive.

Complete parent-child demo. App.js:

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

function OrderStatusChild({ status }) {
  const prevStatusRef = useRef();
  const [changeLog, setChangeLog] = useState([]);

  useEffect(() => {
    if (prevStatusRef.current !== status) {
      setChangeLog(log => [...log, `Status changed from "${prevStatusRef.current || 'NEW'}" to "${status}"`]);
    }
    prevStatusRef.current = status;  // Update for next time
  }, [status]);

  return (
    <div style={{ padding: '15px', border: '1px solid #ccc', margin: '10px 0' }}>
      <h3>Child: Current Status - {status}</h3>
      <ul>
        {changeLog.slice(-3).map((log, i) => <li key={i}>{log}</li>)}
      </ul>
    </div>
  );
}

function OrderParent() {
  const [status, setStatus] = useState('PENDING');
  return (
    <div style={{ padding: '20px' }}>
      <h2>Parent Controls Order Status</h2>
      <select value={status} onChange={(e) => setStatus(e.target.value)}>
        <option value="PENDING">PENDING</option>
        <option value="PACKING">PACKING</option>
        <option value="SHIPPED">SHIPPED</option>
      </select>
      <OrderStatusChild status={status} />
    </div>
  );
}

function App() {
  return <OrderParent />;
}

export default App;

Switch statuses—child detects exact changes using prevStatusRef! Ties perfectly to lifted state patterns without extra effects.​

Intervals & Timers: Backroom Pick Timers

Order systems need countdowns for pick timeouts or auto-save throttles. useRef stores interval IDs to cleanly stop them, avoiding memory leaks.

Full timer example for shipment pick windows. App.js:

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

function PickTimer() {
  const intervalRef = useRef(null);
  const [timeLeft, setTimeLeft] = useState(300);  // 5 min
  const [isActive, setIsActive] = useState(false);

  const startPickTimer = () => {
    setIsActive(true);
    setTimeLeft(300);
    intervalRef.current = setInterval(() => {
      setTimeLeft(prev => {
        if (prev <= 1) {
          clearInterval(intervalRef.current);
          setIsActive(false);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);
  };

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

  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h2>Backroom Pick Timer (5 min window)</h2>
      <div style={{ fontSize: '48px', color: timeLeft < 60 ? 'red' : 'green' }}>
        {Math.floor(timeLeft / 60)}:{(timeLeft % 60).toString().padStart(2, '0')}
      </div>
      {!isActive ? (
        <button onClick={startPickTimer}>Start Pick Window</button>
      ) : (
        <button onClick={stopTimer}>Complete Pick</button>
      )}
      <p>Interval ID stored in ref: {intervalRef.current ? 'Active' : 'Stopped'}</p>
    </div>
  );
}

function App() {
  return <PickTimer />;
}

export default App;

Start the timer—watch it count down precisely. intervalRef ensures safe cleanup on unmount or stop.​

Scroll to Specific Orders: Long Lists

In shipment queues, scroll to newly added or errored orders. useRef grabs the container, .scrollIntoView() does the magic.

Runnable infinite-ish order list. App.js:

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

function ShipmentQueue() {
  const queueRef = useRef(null);
  const [shipments, setShipments] = useState([
    { id: 1, status: 'PENDING', error: false },
    { id: 2, status: 'PACKING', error: false }
  ]);
  const nextIdRef = useRef(3);

  const addNewShipment = () => {
    const newShipment = { id: nextIdRef.current++, status: 'PENDING', error: Math.random() > 0.7 };
    setShipments(prev => [...prev, newShipment]);
    
    // Scroll to new item after render
    setTimeout(() => {
      const newElement = queueRef.current.querySelector(`[data-id="${nextIdRef.current - 1}"]`);
      if (newElement) newElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }, 0);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>Shipment Queue (Scrolls to New)</h2>
      <button onClick={addNewShipment}>Add New Shipment</button>
      <div
        ref={queueRef}
        style={{
          height: '400px',
          overflowY: 'auto',
          border: '2px solid #ddd',
          marginTop: '20px',
          padding: '10px'
        }}
      >
        {shipments.map(shipment => (
          <div
            key={shipment.id}
            data-id={shipment.id}
            style={{
              padding: '15px',
              margin: '5px 0',
              background: shipment.error ? '#fee' : '#efe',
              borderRadius: '4px'
            }}
          >
            Shipment #{shipment.id}: {shipment.status} {shipment.error && '(ERROR - Scroll Target)'}
          </div>
        ))}
      </div>
    </div>
  );
}

function App() {
  return <ShipmentQueue />;
}

export default App;

Add shipments—auto-scrolls to new ones, highlighting errors. nextIdRef tracks sequence mutably.​

Combining with useCallback: Performance Wins

useRef + useCallback prevents child re-renders in deep order hierarchies. Store callbacks in refs for stable references.

Example: Stable callback for bulk order actions. App.js:

import React, { useRef, useState, useCallback } from 'react';

function BulkOrderAction({ onBulkUpdate }) {
  return (
    <button onClick={onBulkUpdate}>Bulk Update 10 Orders</button>
  );
}

function OrderDashboard() {
  const bulkCallbackRef = useRef();
  const [orders, setOrders] = useState(Array(5).fill('PENDING'));

  const handleBulkUpdate = useCallback(() => {
    setOrders(prev => Array(10).fill('UPDATED'));
    console.log('Bulk update fired!');
  }, []);

  bulkCallbackRef.current = handleBulkUpdate;  // Always latest

  return (
    <div style={{ padding: '20px' }}>
      <h2>Dashboard with Stable Bulk Action</h2>
      <BulkOrderAction onBulkUpdate={bulkCallbackRef.current} />
      <ul>
        {orders.map((status, i) => (
          <li key={i}>Order {i + 1}: {status}</li>
        ))}
      </ul>
    </div>
  );
}

function App() {
  return <OrderDashboard />;
}

export default App;

Callback stays stable—child doesn’t re-render unnecessarily. Key for large fulfillment trees.​

Common Pitfalls: Refs Aren't Reactive

Forget ref.current doesn't trigger re-renders? Your display lags. Always sync to state explicitly. Also, don't overuse—useState for UI, useRef for side effects/DOM.

In order timers, forgetting clearInterval(ref.current) leaks memory. Always cleanup in useEffect return.

useRef vs useState: Quick Order Management Guide

ScenariouseRefuseState
DOM focus/measure✅ Perfect❌ Not for DOM
Mutable counters (no UI update)✅ No re-render❌ Causes updates
Previous values✅ Persists silentlyNeeds extra effect
Intervals/timer IDs✅ Stable storage❌ Re-renders break
Internal app flags✅ Mutable magic❌ Overkill

Choose useRef for "doesn't need screen update."​

Pro Tips for Order Apps

  • Form resetinputRef.current.value = '' after submit.
  • Lazy inituseRef(initialValue)—runs once.
  • Multiple refs: Array of refs for dynamic order rows.
  • With portals: Refs work across modals for order previews.
  • Testingref.current accessible in shallow mounts.

Why Developers ❤️ useRef (Real Talk)

useRef feels like having an invisible notepad in your component—jot down DOM handles, counters, or callbacks without React nagging about re-renders. In frantic order fulfillment, when a scanner input needs instant focus or a timer mustn't leak, useRef saves the day quietly. It's the hook that separates junior shuffling from senior smoothness.

Those "aha" moments? Realizing you can track a user's last keystroke for debounced searches or measure a modal's height for perfect positioning—pure developer joy.

Closing Thoughts: Refs Power Real Apps

useRef bridges React's declarative world with imperative needs, keeping your order management apps responsive and intuitive. From focused inputs to leak-free timers, it's essential for production polish.

Pro confession: Early on, I stored UI state in refs and wondered why nothing updated. Now? Refs are my go-to for anything "internal." Tinker with these examples—break them, fix them, own them.

Next up: "Building your first custom hook (form handling)" – We'll craft reusable validation magic. Stay tuned!​