I have a React component that uses useImperativeHandle to expose a Google Maps Street View panorama instance. My intention is for the StreetView ref to be attached to the Google Maps ref after it is available, because after mounting, the first call to useEffect (with an empty dependency array) has streetViewRef.current.panorama
as undefined.
I was able to get to this to work by adding streetViewRef.current?.panorama
to the useEffect dependencies (in that the effect reruns after the value is set), but I don't have a great understanding of why this works (and was under the impression that refs weren't to be used with effects).
// StreetView.tsx
const StreetView = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
updateView,
panorama: streetViewPanoramaRef.current,
}));
// ... rest of component
});
// Parent.tsx
const ParentComponent = () => {
const streetViewRef = useRef<StreetViewHandle>(null);
const mapRef = useRef<GoogleMap>(null);
useEffect(() => {
if (streetViewRef.current && mapRef.current?.map) {
streetViewRef.current.updateView(/* ... */);
mapRef.current.map.setStreetView(streetViewRef.current.panorama);
}
}, [streetViewRef.current?.panorama]); // Why is this okay?
return (
<>
<GoogleMap ref={mapRef} />
<StreetView ref={streetViewRef} />
</>
);
};
I have the following questions:
streetViewRef.current
in the dependency array, but why does including streetViewRef.current?.panorama
solve my problem?Thanks :)
The ref.current
is a stable object that isn't replaced after it's created. This means that after the first render, it won't trigger useEffect
. The panorama
property changes, and that's why it triggers the useEffect
.
However, changing a property on ref.current
doesn't cause a re-render, so in you case something else is causing a re-render. This means that if you remove the cause for re-render, the useEffect
won't be called.
The linter knows that since the ref isn't replaced, the ref in the closure is never stale (points to a previous instance), so it ignores it. The linter also doesn't require a set state function, because it knows it's stable.
When you the ref change to trigger a re-render, you can use useState
and pass the set state function as a ref callback function. When both components are mounted, they set their refs, and trigger the useEffect
. Only when both are ready the useEffect
's condition is true
.
Sample code (not tested):
// StreetView.tsx
const StreetView = forwardRef((props, ref) => {
// function to update parent when ready
const streetViewPanorama = useCallback(panorama => {
ref({
updateView,
panorama
})
}, [ref]);
return (
<div ref={streetViewPanorama}>
content
</div>
)
});
// Parent.tsx
const ParentComponent = () => {
const [streetViewRef, setStreetViewRef] = useState<StreetViewHandle>(null);
const [mapRef, setmapRef] = useState<GoogleMap>(null);
useEffect(() => {
if (streetViewRef && mapRef) {
streetViewRef.updateView(/* ... */);
mapRef.map.setStreetView(streetViewRef.panorama);
}
}, [streetViewRef, mapRef]);
return (
<>
<GoogleMap ref={mapRef} />
<StreetView ref={streetViewRef} />
</>
);
};