Search code examples
reactjstypescriptreact-hooksarcgis-js-api

Functional Component Not Re-rendering after data is updated


I am building a dashboard filled with Esri maps that are editable. The structure of the components is something like this:

<Dashboard>
 <Visuals>
  <EventsMap>
   <PointsLayer/>
  </EventsMap>
 </Visuals>
</Dashboard>

When a user edits something inside of the Dashboards component(i.e. the color of the points on a map) the data does get passed through to PointsLayer which then should re-render and show the updated color, but only updates if I refresh the page. Is it because I don't have a render method? The PointsLayer component:

import {useState, useEffect} from 'react';
import {loadModules} from 'esri-loader';
import {getFormattedDate} from 'Lib/helpers';
import styles from './Summary.module.css';
import point from "@arcgis/core/geometry/Point";


const PointsLayer = (props) => {
    const data = props.data;
    const color = data?.color;
    console.log(color)

    const humanize = (str) =>{
        let i, frags = str.split('_');
        for (i=0; i<frags.length; i++) {
            frags[i] = frags[i].charAt(0).toUpperCase() + frags[i].slice(1);
        }
        return frags.join(' ');
    }

    const pluralize = (val, word, plural = word + 's') => {
        const _pluralize = (num, word, plural = word + 's') =>
            [1, -1].includes(Number(num)) ? word : plural;
        if (typeof val === 'object') return (num, word) => _pluralize(num, word, val[word]);
        return _pluralize(val, word, plural);
    };


    const [graphic, setGraphic] = useState(null);
    useEffect(() => {


        loadModules(['esri/Graphic']).then(([Graphic]) => {

            // Parse out the Lat-Long from each Event and the doc_count
            for (let i = 0; i < data?.events.length; i++) {
                const point = {
                    type: "point", // autocasts as new Point
                    longitude: data?.events[i]?.location.split(",")[1],
                    latitude: data?.events[i]?.location.split(",")[0]
                };


                // Create a symbol for rendering the graphic
                const symbol = {
                    type: "simple-marker",  // autocasts as new SimpleMarkerSymbol()
                    style: "circle", color: color, // Color Selected on popup
                    size: "12px", outline: {
                        color: [255, 255, 255], // White
                        width: 1.5
                    }
                };


                // Create attributes for popup
                const attributes = {
                    watcherType: humanize(data?.events[i]?.doc_fields?.watcher_type),
                    eventCount: data?.events[i]?.doc_count,
                    plural: pluralize(data?.events[i]?.doc_count, 'Event'),
                    deviceName: data?.events[i]?.key,
                    lat: data?.events[i]?.location.split(",")[0],
                    long: data?.events[i]?.location.split(",")[1],
                    account: data?.events[i]?.doc_fields?.account_id,
                    address: data?.events[i]?.doc_fields['@service_address'],
                    meterId: data?.events[i]?.doc_fields?.meter_id,
                    lastEvent: getFormattedDate(data?.events[i]?.doc_fields['@time_raised_last'], '')
                };

                // Create popup template
                const popupTemplate = {
                    title: "{eventCount} {watcherType} {plural}",
                    content:
                        "<ul><li><b>Address:</b> {address}</li>" +
                        "<li><b>Account ID:</b> {account}</li>" +
                        "<li><b>Meter ID:</b> {meterId}</li>" +
                        "<li><b>Last Event:</b> {lastEvent}</li>" +
                        "<li><a href='https://maps.google.com/maps?q=&layer=c&cbll={lat},{long}&cbp='>Google Street View</a></li></ul>"
                };

                // Add the multiPoints to a new graphic
                const graphic = new Graphic({
                    geometry: point,
                    symbol: symbol,
                    attributes: attributes,
                    popupTemplate: popupTemplate
                });
                setGraphic(graphic);
                props.view.graphics.add(graphic);
            }
        }).catch((err) => console.error(err));

        return function cleanup() {
            props.view.graphics.remove(graphic);
        };
    }, []);

    return null;

}

export default PointsLayer

An image to visualize what I am working on: enter image description here


Solution

  • Try adding the graphic state to the useEffect dependency array [graphic]

    useEffect(function, [graphic]) or useEffect(function, [props]) but props may cause more re-renders than you may want

    A more complete example would look like this.

        const [graphic, setGraphic] = useState(null);
    useEffect(() => {
    
    
        loadModules(['esri/Graphic']).then(([Graphic]) => {
    
            // Parse out the Lat-Long from each Event and the doc_count
            for (let i = 0; i < data?.events.length; i++) {
                const point = {
                    type: "point", // autocasts as new Point
                    longitude: data?.events[i]?.location.split(",")[1],
                    latitude: data?.events[i]?.location.split(",")[0]
                };
    
    
                // Create a symbol for rendering the graphic
                const symbol = {
                    type: "simple-marker",  // autocasts as new SimpleMarkerSymbol()
                    style: "circle", color: color, // Color Selected on popup
                    size: "12px", outline: {
                        color: [255, 255, 255], // White
                        width: 1.5
                    }
                };
    
    
                // Create attributes for popup
                const attributes = {
                    watcherType: humanize(data?.events[i]?.doc_fields?.watcher_type),
                    eventCount: data?.events[i]?.doc_count,
                    plural: pluralize(data?.events[i]?.doc_count, 'Event'),
                    deviceName: data?.events[i]?.key,
                    lat: data?.events[i]?.location.split(",")[0],
                    long: data?.events[i]?.location.split(",")[1],
                    account: data?.events[i]?.doc_fields?.account_id,
                    address: data?.events[i]?.doc_fields['@service_address'],
                    meterId: data?.events[i]?.doc_fields?.meter_id,
                    lastEvent: getFormattedDate(data?.events[i]?.doc_fields['@time_raised_last'], '')
                };
    
                // Create popup template
                const popupTemplate = {
                    title: "{eventCount} {watcherType} {plural}",
                    content:
                        "<ul><li><b>Address:</b> {address}</li>" +
                        "<li><b>Account ID:</b> {account}</li>" +
                        "<li><b>Meter ID:</b> {meterId}</li>" +
                        "<li><b>Last Event:</b> {lastEvent}</li>" +
                        "<li><a href='https://maps.google.com/maps?q=&layer=c&cbll={lat},{long}&cbp='>Google Street View</a></li></ul>"
                };
    
                // Add the multiPoints to a new graphic
                const graphic = new Graphic({
                    geometry: point,
                    symbol: symbol,
                    attributes: attributes,
                    popupTemplate: popupTemplate
                });
                setGraphic(graphic);
                props.view.graphics.add(graphic);
            }
        }).catch((err) => console.error(err));
    
        return function cleanup() {
            props.view.graphics.remove(graphic);
        };
    }, [graphic]);
    

    or using props,

        const [graphic, setGraphic] = useState(null);
    useEffect(() => {
    
    
        loadModules(['esri/Graphic']).then(([Graphic]) => {
    
            // Parse out the Lat-Long from each Event and the doc_count
            for (let i = 0; i < data?.events.length; i++) {
                const point = {
                    type: "point", // autocasts as new Point
                    longitude: data?.events[i]?.location.split(",")[1],
                    latitude: data?.events[i]?.location.split(",")[0]
                };
    
    
                // Create a symbol for rendering the graphic
                const symbol = {
                    type: "simple-marker",  // autocasts as new SimpleMarkerSymbol()
                    style: "circle", color: color, // Color Selected on popup
                    size: "12px", outline: {
                        color: [255, 255, 255], // White
                        width: 1.5
                    }
                };
    
    
                // Create attributes for popup
                const attributes = {
                    watcherType: humanize(data?.events[i]?.doc_fields?.watcher_type),
                    eventCount: data?.events[i]?.doc_count,
                    plural: pluralize(data?.events[i]?.doc_count, 'Event'),
                    deviceName: data?.events[i]?.key,
                    lat: data?.events[i]?.location.split(",")[0],
                    long: data?.events[i]?.location.split(",")[1],
                    account: data?.events[i]?.doc_fields?.account_id,
                    address: data?.events[i]?.doc_fields['@service_address'],
                    meterId: data?.events[i]?.doc_fields?.meter_id,
                    lastEvent: getFormattedDate(data?.events[i]?.doc_fields['@time_raised_last'], '')
                };
    
                // Create popup template
                const popupTemplate = {
                    title: "{eventCount} {watcherType} {plural}",
                    content:
                        "<ul><li><b>Address:</b> {address}</li>" +
                        "<li><b>Account ID:</b> {account}</li>" +
                        "<li><b>Meter ID:</b> {meterId}</li>" +
                        "<li><b>Last Event:</b> {lastEvent}</li>" +
                        "<li><a href='https://maps.google.com/maps?q=&layer=c&cbll={lat},{long}&cbp='>Google Street View</a></li></ul>"
                };
    
                // Add the multiPoints to a new graphic
                const graphic = new Graphic({
                    geometry: point,
                    symbol: symbol,
                    attributes: attributes,
                    popupTemplate: popupTemplate
                });
                setGraphic(graphic);
                props.view.graphics.add(graphic);
            }
        }).catch((err) => console.error(err));
    
        return function cleanup() {
            props.view.graphics.remove(graphic);
        };
    }, [props]);
    

    I would think just using the graphic state would be closer to what you are looking for. Leaving the dependency array empty causes the useEffect hook to only fire on the initial component mount.

    Adding the graphic state to the array tells the useEffect to watch for changes in the graphic state, and if it changes, refire the useEffect again

    This post may be helpful Hooks and Dependency Arrays