useRef value is undefined on initial render

Viewed 7590

I'm making a map with react and react-leaflet. React Leaflet has a class called FeatureGroup, which I want to access on the first render using the useRef hook from React and then accessing the value in a useEffect function. However on initial render the ref is undefined (markerRef.current returns null). Only after I make any sort of changes to the code, save and the React app reloads it gets the value

Can you tell me what I'm doing wrong and how I can make it so that markerRef.current is not null on the initial render?

Please find the abbreviated code for the component below:

import {useRef} from 'react';
import {FeatureGroup, //... } from 'react-leaflet';

const MapView = ({breweries}) => {
  const markerGroupRef = useRef(null);
  //...
  useEffect(() => {
    if(markerGroupRef.current) {
      //here I want to access a method called getBounds() which is in the markerGroupRef.current object 
      //but markerGroupRef.current has null value and so it doesn't execute
      //when making a save change and react app reloads it has the FeatureGroup class as value as expected
      console.log(markerGroupRef.current)
    }
  }, [markerGroupRef])
  //...
  return (
            <FeatureGroup ref={markerGroupRef}>
              {breweries && breweries.map(brewery => <Brewery key={breweries.indexOf(brewery)} brewery={brewery}/>)}
              {breweryStore && breweryStore.searchLocation &&  <LocationMarker markerPosition={{lat: breweryStore.searchLocation.lat, lng: breweryStore.searchLocation.lon}}/>}
            </FeatureGroup>
    );
  }

4 Answers

References create by useRef do not trigger component rerenders. So the useRef hook you created will never execute. Also since you initialized the ref to null, at the start of the first render, it will remain null. During the first render, the ref={markerGroupRef} on FeatureGroup will update it, but again it won't trigger another render. State must be modified to trigger any possible rerenders.

You might want to use a callback ref as indicated here

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

and justified here here

Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.

Your problem is that updating a ref doesn't make the component re-render, so useEffect only ever runs once and on that first render the ref is still null.

What you describe is the correct behaviour and you will need to wait for a re-render to do anything with the ref. If you force a re-render you will be able to use the ref.current property:

    const MapView = ({ breweries }) => {
        const markerGroupRef = useRef(null);
        const [refAquired, setRefAquired] = useState(false)
        //...
        useEffect(() => {
            setRefAquired(true)
        }, []);

        useEffect(()=> {
            console.log(markerGroupRef.current)
        }, [refAquired])

This is a crude demonstration and you should try to make the render cycles tie in to you other rendering and business logic.

As an alternative, you can force a component to render again immediately following its initial render via a useEffect hook, which ensures access to the element. I'm not sure if this is best practice, but honestly the performance implications are minimal.

const [shouldUpdate, setShouldUpdate] = useState(true)

// set shouldUpdate => true on initial render, triggering re-render
useEffect(() => {
  if (shouldUpdate) setShouldUpdate(false)
}, [shouldUpdate])

Alright, after fiddling around I figured it out. Instead of using useRef directly and then trying to access its value immediately with useEffect hook I define the ref with useState and then set the value of that state in the ref property of FeatureGroup using a callback function.

So instead of:

const markerGroupRef = useRef(null)

I now have:

const [markerGroupRef, setMarkerGroupRef] = useState(null)

And then in the render I now have:

<FeatureGroup ref={ref => setMarkerGroupRef(ref)}>...</FeatureGroup>

I can then access the FeatureGroup class as expected in a useEffect hook like so:

 useEffect(() => {
    if(markerGroupRef) {
      markerGroupRef.getBounds()
    }
  }, [markerGroupRef])
Related