Typical Usage about useRef

Typical Usage about useRef

·

3 min read

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>
    );
}