5

I'm fairly new to the useMemo and useCallback hooks within react and I'm kind of stuck as to why my functions still continue to re-render even if the variable I'm checking against stays the same. Currently I'm trying to build a google map that allows the user to turn on and off certain markers that are displayed on the map. My functional component should only have to render these markers once on load and then memoize them after that since they are just being turned on/off afterwards. Now I understand the concept that hooks such as these only do a shallow comparison, but I'm comparing boolean values which I would think these hooks would be able to understand.

This is my current map file:

// constants are above

function Map() {

    const {isLoaded} = useJsApiLoader({
        googleMapsApiKey: env.GOOGLE_MAPS_API.API_KEY,
        libraries: libs
    }) 

    const [map, setMap] = useState(null);
    const [places,setPlaces] = useState([]);
    const [places2,setPlaces2] = useState([]);
    const [heatMap,setHeatMap] = useState([]);
    const [fetchingData, setFetchingData] = useState(true);
    const [showPlaces,setShowPlaces] = useState(true);
    const [showPlaces2,setShowPlaces2] = useState(false);
    const [showHeatMap,setShowHeatMap] = useState(false);

    useEffect(() => {
        let mounted = true;

        if(mounted){
            socket.emit('getMapAnalytics', ([heatmap,places,places2]) => {
                console.log({heatmap})
                setPlaces(places);
                setPlaces2(places2);
                setHeatMap(heatmap);
                setFetchingData(false);
            });            
        }

        return () => {
            mounted = false;
        }
    },[])

    const onLoad = useCallback((map) => {
        setMap(map);
    },[])

    const onUnmount = useCallback((map) => {
        setMap(null);
    },[]);

    const renderPlacesMarkers = useCallback(() => {
        console.log('rendering places')
        if(showPlaces){
            return places.map(place => (
                <Marker icon={icon} key={place.ID} title={place.Name} position={{lat:place.Latitude,lng:place.Longitude}}/>
            ))
        }
        return null;
    },[showPlaces]);

    const renderPlaces2Markers = useCallback(() => {
        console.log('rendering places2')
        if(showPlaces2){
            return places2.map(place => (
                <Marker key={place.ID} title={place.ID} position={{lat:place.Latitude,lng:place.Longitude}}/>
            ))
        }
        return null;
    },[showPlaces2]);

    const renderHeatMap = useCallback(() => {
        console.log('rendering heat map')
        if(showHeatMap){
            const mapData = heatMap.map(location => ({
                location: new google.maps.LatLng(location.Latitude,location.Longitude), weight: location.population_density
            }))

            return (<HeatmapLayer data={mapData} options={{gradient: gradient}} />)
        }
        return null;
    },[showHeatMap]);

    const onChecked = (e) => {
        switch(e.target.id){
            case 'places': setShowPlaces(e.target.checked);
                break;
            case 'places2': setShowPlaces2(e.target.checked);
                break;
            case 'heat-map': setShowHeatMap(e.target.checked);
                break;
        }
    }

    return isLoaded && !fetchingData ? (
        <div id="map-container">
            <div id="top-bar" style={{display: 'flex', justifyContent:'space-evenly'}}>
                <div style={{display: 'flex'}}>
                    <input type="checkbox" id="places" checked={showPlaces} onChange={onChecked}></input><label>techs</label>
                </div>
                <div style={{display: 'flex'}}>
                    <input type="checkbox" id="places2" checked={showPlaces2} onChange={onChecked}></input><label>kiosks</label>
                </div>
                <div style={{display: 'flex'}}>
                    <input type="checkbox" id="heat-map" checked={showHeatMap} onChange={onChecked}></input><label>heatmap</label>
                </div>
            </div>
            <GoogleMap mapContainerStyle={containerStyle} center={center} zoom={10} onLoad={onLoad} onUnmount={onUnmount}>
                {renderPlacesMarkers()}
                {renderPlaces2Markers()}
                {renderHeatMap()}
            </GoogleMap>
        </div>        
    ) : <Spinner margin="margin-25"></Spinner>
}

export default memo(Map);

I've tried setting the useCallback parameter to something else such as the array length of places and places2 but that still ended up re-rendering each function still.

The only thing that I could think of that is making these methods re-render each time is because I'm returning an array of JSX elements, but I could not find good examples of this online. If anyone could point me in the right direction to rightfully understand why this is happening, that would be great. TIA!

Michael
  • 1,454
  • 3
  • 19
  • 45

1 Answers1

4

It seems you are misunderstanding useCallback.

If you useCallback some function, it will not be re-created if the dependencies don't change. But if you invoke it manually, it will always run.

When you click any of the checkboxes, the Map gets re rendered. If a dependency of say renderPlaces, wasn't changed, the function will remain the same, but afterwards you are manually invoking it in render anyway: renderPlaces().

From your setup my advice would be to instead make renderPlaces and two other functions as normal components (and define them outside the Map component) and apply React.memo on them.

In general I would advise to render components instead of calling them, e.g. better use <Component/> rather than Component(). And defining functional components inside functional components is a bad idea because reacts reconciliation algorithm will think they are different type on each render (because if you define function inside function, it will be re-created each time).

Giorgi Moniava
  • 27,046
  • 9
  • 53
  • 90
  • Yes, so the methods ```renderPlacesMarkers, renderPlaces2Markers, & renderHeatMap``` are just to display the markers on the map when they are toggled on/off. Since these markers are static each time, I don't want to have to reiterate through the arrays to show them in the same place again. If that makes sense. – Michael Jul 09 '21 at 16:27
  • Just so I get this right, you're saying that it would be better to just create separate components for each of those functions instead? – Michael Jul 09 '21 at 16:29
  • @Michael Yes, you can apply `React.memo` on them so they don't re render if the props don't change. Try to understand all I've said in the answer because there were other points mentioned too. – Giorgi Moniava Jul 09 '21 at 16:32
  • I appreciate the feedback. This should improve the way I think about rendering React components in my application. I will try this out. – Michael Jul 09 '21 at 16:40