DEV Community

Cover image for React: 5 Small (Yet Easily Fixable) Mistakes Junior Frontend Developers Make With React Refs
Ndeye Fatou Diop
Ndeye Fatou Diop

Posted on • Edited on • Originally published at frontendjoy.com

React: 5 Small (Yet Easily Fixable) Mistakes Junior Frontend Developers Make With React Refs

I have reviewed more than 1,000 front-end pull requests.

Like many junior developers, I made some common mistakes when I started, especially while using refs.

If you're in the same boat, here are 6 small mistakes you can quickly fix to use refs properly in React:

Mistake #1: Using a state when a ref would be a better choice

One common mistake is having a state when a ref would have been more suitable. Since ref updates don't trigger re-renders, they are perfect for situations where this behavior is unnecessary, resulting in better app performance.

Mistake 1


❌ Bad: We are storing the interval in the state and triggering an unnecessary re-render.

import { useState, useEffect } from "react";

function Timer() {
  const [time, setTime] = useState(new Date());
  const [intervalId, setIntervalId] = useState();

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    // Storing the id in the state
    setIntervalId(intervalId);
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  const stopTimer = () => {
    intervalId && clearInterval(intervalId);
  };

  return (
    <>
      <h2>Current time: {time.toLocaleTimeString()}</h2>
      <button onClick={stopTimer}>Stop timer</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Good: We store the interval ID in a ref and don't trigger a re-render.

import { useState, useEffect, useRef } from "react";

function Timer() {
  const [time, setTime] = useState(new Date());
  const intervalRef = useRef(undefined);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    intervalRef.current = intervalId;
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  const stopTimer = () => {
    intervalRef.current && clearInterval(intervalRef.current);
  };

  return (
    <>
      <h2>Current time: {time.toLocaleTimeString()}</h2>
      <button onClick={stopTimer}>Stop timer</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Mistake #2: Using ref.current vs. ref or using the ref value before it's set

When I first began, I often stumbled upon this mistake. I would use ref.current before the value was actually passed, or I'd pass ref.current to my functions instead of using ref directly. In the latter case, the ref object would change while I still held onto the value stored in ref.current.

Mistake 2


❌ Bad: The code below won't work since ref.current is null initially. As a result, when the effect runs, element is null.

import { useState, useEffect, useRef } from "react";

export default function App() {
  const ref = useRef();
  const isHovered = useIsHovered(ref.current);

  return (
    <div ref={ref}>
      Hovered: {`${isHovered}`}
    </div>
  );
}

function useIsHovered(element) {
  const [isHovered, setIsHovered] = useState(false);
  useEffect(() => {
    if (element == null) {
      return;
    }
    const onMouseEnter = () => setIsHovered(true);
    const onMouseLeave = () => setIsHovered(false);
    element.addEventListener("mouseenter", onMouseEnter);
    element.addEventListener("mouseleave", onMouseLeave);
    return () => {
      element.removeEventListener("mouseenter", onMouseEnter);
      element.removeEventListener("mouseleave", onMouseLeave);
    };
  }, [element]);
  return isHovered;
}
Enter fullscreen mode Exit fullscreen mode

✅ Good: The code below will work because since we pass ref to useIsHovered, when the effect runs ref.current is well defined.

import { useState, useEffect, useRef } from "react";

export default function App() {
  const ref = useRef();
  const isHovered = useIsHovered(ref);

  return (
    <div ref={ref}>
      Hovered: {`${isHovered}`}
    </div>
  );
}

function useIsHovered(ref) {
  const [isHovered, setIsHovered] = useState(false);
  useEffect(() => {
    if (ref.current == null) {
      return;
    }
    const onMouseEnter = () => setIsHovered(true);
    const onMouseLeave = () => setIsHovered(false);
    ref.current.addEventListener("mouseenter", onMouseEnter);
    ref.current.addEventListener("mouseleave", onMouseLeave);
    return () => {
      ref.current.removeEventListener("mouseenter", onMouseEnter);
      ref.current.removeEventListener("mouseleave", onMouseLeave);
    };
  }, [ref]);
  return isHovered;
}
Enter fullscreen mode Exit fullscreen mode

Mistake #3: Forgetting to use forwardRef

We probably all made this mistake. In fact, React doesn't let you pass a ref to a function component unless it's wrapped with forwardRef. The fix? Simply wrap the component receiving the ref in forwardRef or use another name for your ref prop.

Mistake 3


❌ Bad: The code below won't work since function components like NameInput cannot be given refs this way.

import { useState, useEffect, useRef } from "react";

export default function App() {
  const ref = useRef();
  useEffect(function autoFocusInput() {
    ref.current?.focus();
  }, []);

  return (
    <>
      Name: <NameInput ref={ref} />
    </>
  );
}

const NameInput = ({ ref }) => {
  const [value, setValue] = useState("");
  return (
    <input ref={ref} value={value} onChange={(e) => setValue(e.target.value)} />
  );
};
Enter fullscreen mode Exit fullscreen mode

✅ Good: We wrap the component in forwardRef.

import { useState, useEffect, useRef, forwardRef } from "react";

export default function App() {
  const ref = useRef();
  useEffect(function autoFocusInput() {
    ref.current?.focus();
  }, []);

  return (
    <>
      Name: <NameInput ref={ref} />
    </>
  );
}

const NameInput = forwardRef((_, ref) => {
  const [value, setValue] = useState("");
  return (
    <input ref={ref} value={value} onChange={(e) => setValue(e.target.value)} />
  );

Enter fullscreen mode Exit fullscreen mode

Mistake #4: Initializing the ref with an "expensive" function call

When you call a function to set the ref initial value, that function will be called with each render. If the function is expensive, this will unnecessarily affect your app performance. The solution? Memoize the function or initialize the ref during render (after checking that values are not set yet).

Mistake 4


❌ Bad: We will read from the local storage whenever this component re-renders (e.g., when inputValue changes).

import { useState, useRef, useEffect } from "react";

export default function App() {
  const ref = useRef(window.localStorage.getItem("demo-last-connected"));
  const [inputValue, setInputValue] = useState("");

  useOnBeforeUnload(() => {
    const date = new Date().toUTCString();
    console.log("Date", date);
    window.localStorage.setItem("demo-last-connected", date);
  });

  return (
    <>
      <div>
        Last connected: <strong>{ref.current}</strong>
      </div>
      Name:{" "}
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
    </>
  );
}

function useOnBeforeUnload(callback) {
  useEffect(() => {
    window.addEventListener("beforeunload", callback);
    return () => window.removeEventListener("beforeunload", callback);
  }, [callback]);
}
Enter fullscreen mode Exit fullscreen mode

✅ Good: We read from the local storage once.

export default function App() {
  const ref = useRef(null);
  if (ref.current === null) {
    ref.current = window.localStorage.getItem("demo-last-connected");
  }
  const [inputValue, setInputValue] = useState("");

  useOnBeforeUnload(() => {
    const date = new Date().toUTCString();
    console.log("Date", date);
    window.localStorage.setItem("demo-last-connected", date);
  });

  return (
    <>
      <div>
        Last connected: <strong>{ref.current}</strong>
      </div>
      Name:{" "}
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
    </>
  );
}
/// ...
Enter fullscreen mode Exit fullscreen mode

Mistake #5: Using a ref callback function that changes on every render

Ref callbacks can tidy up your code. However, be aware that React calls the ref callback whenever it changes. This means that when a component re-renders, the previous function is called with null as the argument, while the next function is called with the DOM node. This can lead to some unwanted flickering in the UI. The solution? Make sure to memoize the ref callback function.

Mistake 5


❌ Bad: The code below won't work properly because whenever inputValue or currentTime changes, the ref callback function will run again, and the input will be focused again.

import { useEffect, useState } from "react";

export default function App() {
  const ref = (node) => {
    node?.focus();
  };
  const [nameValue, setNameValue] = useState("");
  const currentTime = useCurrentTime();

  return (
    <>
      <h2>Time {currentTime}</h2>
      <label htmlFor="name">Name: </label>
      <input
        id="name"
        ref={ref}
        value={nameValue}
        onChange={(e) => setNameValue(e.target.value)}
      />
    </>
  );
}

function useCurrentTime() {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    return () => clearInterval(intervalId);
  });
  return time.toString();
}
Enter fullscreen mode Exit fullscreen mode

✅ Good: The function is wrapped in useCallback, and the input will be focused only when it is mounted.

export default function App() {
  // Wrap in useCallback
  const ref = useCallback((node) => {
    node?.focus();
  }, []);
  const [nameValue, setNameValue] = useState("");
  const currentTime = useCurrentTime();

  return (
    <>
      <h2>Time {currentTime}</h2>
      <label htmlFor="name">Name: </label>
      <input
        id="name"
        ref={ref}
        value={nameValue}
        onChange={(e) => setNameValue(e.target.value)}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Thank you for reading this post 🙏.

Leave a comment 📩 to share a mistake you made with refs and how you overcame it.

And Don't forget to Drop a "💖🦄🔥".

If you're learning React, download my 101 React Tips & Tricks book for FREE.

If you like articles like this, join my FREE newsletter, FrontendJoy.

If you want daily tips, find me on X/Twitter.

Top comments (8)

Collapse
 
maxart2501 profile image
Massimo Artizzu

I appreciate the effort, but keep in mind that posting code as animated gifs is rather unaccessible. Would you consider adding text snippets?

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

I added the code snippets 😃. Hope it is helpful !

Collapse
 
maxart2501 profile image
Massimo Artizzu

Great job!

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks for the suggestion ! Yes indeed I can definitely do so !

Collapse
 
dsaga profile image
Dusan Petkovic

Interesting article, how do you make these animated code gifs?

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks 🙏 I use keynote for mac

Collapse
 
iamwillpursell profile image
Will Pursell

I would consider myself a little bit further along than a junior dev, but there were a few things that I don’t currently use and should.

One theme I picked up on was just avoiding unnecessary re-renders when possible. I’m looking back in my code and realizing there is a lot of resources (and potentially money) I can save just by saving some data to pull from later.

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

I am so glad that this can be helpful. And honestly I agree that that first mistake can happen at all levels. I have been guilty of this a lot. In fact, it is so easy since well, things work ok