State and effects cover most of what components need. This lesson adds two more tools. React useRef gives a component a place to keep a value that should survive re-renders but should not cause one, and it lets you reach real DOM elements. Custom hooks let you package stateful logic into a function of your own and reuse it across components.
What React useRef gives you
Calling useRef(initialValue) returns a plain object with one property, current. React gives you the same object on every render, so whatever you store in ref.current is still there next time. Unlike state, changing ref.current does not make React render again. That makes refs right for information the screen does not show, such as a timer id.
State (useState) | Ref (useRef) | |
|---|---|---|
| Survives re-renders | Yes | Yes |
| Changing it re-renders | Yes | No |
| How you change it | Call the setter | Assign to ref.current |
| Read during rendering | Yes | No – only in handlers and effects |
| Typical use | Anything shown on screen | DOM elements, timer ids, previous values |
Refs to DOM elements
React manages the DOM for you, but sometimes you need the real element – to move focus, scroll to it, or measure it. Create a ref with useRef(null) and pass it to a JSX element with the ref attribute. After React creates the element, it puts the DOM node in ref.current, and you can call browser methods on it in an event handler.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Focusing an input with a ref</title>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
input { padding: 6px; width: 220px; margin-right: 6px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React, { useRef, useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
function App() {
const inputRef = useRef(null);
const [cities, setCities] = useState([]);
function handleAdd() {
const value = inputRef.current.value.trim();
if (value) {
setCities([...cities, { id: cities.length + 1, name: value }]);
inputRef.current.value = "";
}
inputRef.current.focus(); // keep typing without clicking the field
}
return (
<div>
<label htmlFor="city">City to visit</label><br />
<input id="city" ref={inputRef} defaultValue="Hampi" />
<button type="button" onClick={handleAdd}>Add</button>
<ul>
{cities.map(c => <li key={c.id}>{c.name}</li>)}
</ul>
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>Here the input is uncontrolled (it uses defaultValue), and the ref reads it only when the button is clicked. Moving focus back into the field after an action is a small detail that keyboard users appreciate.
Tip: New in React 19: a function component can receive ref as a normal prop and pass it on, e.g. function TextField({ ref, ...rest }) { return <input ref={ref} {...rest} />; }. Older code wraps such components in forwardRef, which is no longer needed.
Refs to remember values
A stopwatch needs the id returned by setInterval so that the Stop button can clear it. The id is never shown, so storing it in state would only cause pointless renders, and a normal variable would be forgotten on the next render. A ref is the right home for it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A stopwatch that keeps its interval id in a ref</title>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
.time { font-size: 32px; font-variant-numeric: tabular-nums; margin: 8px 0; }
button { margin-right: 6px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React, { useRef, useState, useEffect } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
function App() {
const [ms, setMs] = useState(0);
const [running, setRunning] = useState(false);
const intervalRef = useRef(null);
function start() {
if (running) return;
setRunning(true);
intervalRef.current = setInterval(() => setMs(t => t + 100), 100);
}
function stop() {
clearInterval(intervalRef.current);
setRunning(false);
}
function reset() {
stop();
setMs(0);
}
// stop the timer if the component is removed
useEffect(() => () => clearInterval(intervalRef.current), []);
return (
<div>
<p className="time">{(ms / 1000).toFixed(1)} s</p>
<button type="button" onClick={start} disabled={running}>Start</button>
<button type="button" onClick={stop} disabled={!running}>Stop</button>
<button type="button" onClick={reset}>Reset</button>
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>Watch out: Do not read or write ref.current while rendering (in the body of the component that builds the JSX). React does not know when it changes, so the screen can show out-of-date values. Use refs inside event handlers and effects; if something must appear on screen, it belongs in state.
Writing a custom hook
Imagine two components that both need the current window width. You could copy the same useState and useEffect code into both, but then you must fix bugs twice. Instead, move the code into a function whose name starts with use. That function is a custom hook. It can call other hooks, and it returns whatever the component needs.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A useWindowWidth custom hook</title>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
.box { padding: 10px; border-radius: 8px; background: #eef6fb; margin-top: 8px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React, { useState, useEffect } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return width;
}
function LayoutInfo() {
const width = useWindowWidth();
return <p>Width: <strong>{width}px</strong></p>;
}
function MenuHint() {
const width = useWindowWidth();
return (
<p className="box">
{width < 600 ? "Small screen: show a menu button" : "Wide screen: show the full menu"}
</p>
);
}
function App() {
return (
<div>
<LayoutInfo />
<MenuHint />
<p>Resize the window (or the editor panel) to see both update.</p>
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>Rules for custom hooks
- The name must start with
usefollowed by a capital letter, such asuseWindowWidthoruseOnlineStatus. This tells React and the linter that the rules of hooks apply. - Call it only at the top level of a component or another hook – the same rules as for
useState. - Custom hooks share logic, not state. Each component that calls
useWindowWidth()gets its own independent state. - Only make a function a hook if it calls other hooks. A plain helper such as
formatRupees()should stay a normal function.
Good candidates for custom hooks are pieces of logic you find yourself repeating: loading data from a URL, tracking whether the browser is online, toggling a boolean, or debouncing a search input. You have now seen every core hook a beginner needs; the last lesson, React Project with Vite, moves all of this into a real project.
