state without an update
We can see useRef
as a tool to define a non-reactive state. Compared with normal state defined by useState
, the change of this non-reactive state will not trigger component updating.
function Demo() {
const [count1, setCount1] = useState(0);
const count2 = useRef(0);
return (
<div>
<div>count1: {count1}</div>
<button onClick={() => setCount1((v) => v + 1)}>add1</button>
<br />
<div>count2: {count2.current}</div>
<button onClick={() => (count2.current += 1)}>add2</button>
</div>
);
}
For example, Click on the add1
button will change the count1
state, and will trigger a component update, thus displaying latest view. Click on the add2
button will change the count2
state too, but the view should stay put.
Keeping references to DOM elements
Keeping references to DOM elements is a typical usage for useRef
. In some cases, we need to access real DOM elements to do some operations. For example, we may need to focus on an element, get input element's value, etc. Example below shows how to detect if clicking on the current element.
function Demo() {
const [isIn, setIsIn] = useState(false);
const ref = useRef(null);
useEffect(() => {
const listener = (e) => {
if (!ref.current) return;
setIsIn(ref.current.contains(e.target));
};
window.addEventListener("mousedown", listener);
return () => {
window.removeEventListener("mousedown", listener);
};
}, []);
return (
<div
ref={ref}
style={{ width: "100px", height: "100px", backgroundColor: "red" }}
>
<div> {isIn ? "in" : "out"}</div>
</div>
);
}
Storing timer IDs with a ref
When component is unmouned, the timers should be cancelled as well. We can use useRef
to store timer IDs and use them whem component unmounts. In this way, we could avoid running timers on unmounted components.
export default function App() {
const [isShow, setIsShow] = useState(true);
return (
<div>
<button onClick={() => setIsShow((v) => !v)}>toggle</button>
{isShow && <Demo />}
</div>
);
}
function Demo() {
const [count, setCount] = useState(0);
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setInterval(() => {
setCount((v) => v + 1);
}, 1000);
return () => {
clearInterval(timerRef.current);
};
}, []);
return (
<div>
<div>count: {count}</div>
</div>
);
}
Avoiding memory leaks
When we do some asychronous operations after the component mounts, we may do some state updating operations. Because these operations is to be done after the asynchronous operations, and at that time the components may be already mounted, so this may causes memory leak problems, just like the above timer example.
function Demo() {
const [loading, setLoading] = useState("loading");
useEffect(() => {
fetch("http://yaox023.com")
.then(() => {
// now promise resolve
// if now components is unmounted, then comes the problem
setLoading("success");
})
.catch(() => {
// same thing in here
setLoading("fail");
});
}, []);
return <h1>{loading}</h1>;
}
We can use useRef
hook to define an flag variable to stand as a guard to avoid this kind of cases.
function Demo() {
const [loading, setLoading] = useState("loading");
const mountedRef = useRef(true);
useEffect(() => {
fetch("http://yaox023.com")
.then(() => {
// guard against umount
if (!mountedRef.current) return;
setLoading("success");
})
.catch(() => {
if (!mountedRef.current) return;
setLoading("fail");
});
return () => {
// unmounting, set value false
mountedRef.current = false;
};
}, []);
return <h1>{loading}</h1>;
}
Control Effect
We know that useEffect
hook will run the function after the component mounts and after every components updating. We can use useRef
hook to control this kind of behavior. For example, we stop the mount run of the useEffect
and maintain the update run.
function Demo() {
const [count, setCount] = useState(0);
const flagRef = useRef(false);
useEffect(() => {
if (!flagRef.current) {
flagRef.current = true;
return;
}
console.log(1);
});
return (
<div>
count: {count}
<button onClick={() => setCount((v) => v + 1)}>add</button>
</div>
);
}