3

The size.value state is one option behind when the _updateData function triggers onChange of the <select>. For example, Let's say the starting defaultValue option of <select> is "All", locations is empty...the option is changed to "Medium", locations is "All"...the option is changed to "Large", locations is "Medium"...and so on. Initially I want the _updateData function to run onload with "All" selected which is not working either, it throws the error Cannot read property 'target' of undefined on setSize({value: event.target.value}). What am I doing wrong here? Thanks for the help.

const Map = () => {
    const [viewport, setViewport] = useState({longitude: -98.58, latitude: 39.83, zoom: 3.5})
    const [locations, setLocations] = useState([])
    const [geojson, setGeojson] = useState(null)
    const [size, setSize] = useState({value: "All"})

    useEffect(() => {
        setLocations(geodata)
        _updateData()
    }, []);

    const _updateViewport = viewport => {
        setViewport(viewport)
    }

    const _updateData = event => {
        setSize({value: event.target.value})
        const tempLocations = [];
        locations.forEach(function(res) {
            if (size.value === "All") {
                tempLocations.push(res);
            } else if (res.Size === size.value) {
                tempLocations.push(res);
            }
        });
        var data = {
            ...
        };
        setGeojson(data);
    }

    return (
        <ReactMapGL
            {...viewport}
            onViewportChange={_updateViewport}
            width="100%"
            height="100%"
            mapStyle={mapStyle}
            mapboxApiAccessToken={TOKEN}>
            <Source id="my-data" type="geojson" data={geojson}>
                <Layer {...icon} />
            </Source>
            <div style={navStyle}>
                <NavigationControl onViewportChange={_updateViewport} />
                <select onChange={_updateData} defaultValue={size}>
                    <option value="All">All</option>
                    <option value="Large">Large</option>
                    <option value="Medium">Medium</option>
                    <option value="Small">Small</option>
                    <option value="Very Small">Very Small</option>
                </select>
            </div>
        </ReactMapGL>
    );
}

export default Map;
ckingchris
  • 559
  • 1
  • 12
  • 25

2 Answers2

2

Yes, you are calling your select's onChange handler in the mounting useEffect hook with no event object to dereference a target property. I would factor out the rest of the updateData code so you can call it with the initial state value. This will allow you to update location details on mount using the initial size state date AND the select's onChange will remain as it was previously.

NOTE: You should note that updates to state won't take effect until the next render cycle, so in your code you call setSize with the new value but continue processing locations the current size value, so you need to forward the current value.

const Map = () => {
    const [viewport, setViewport] = useState({longitude: -98.58, latitude: 39.83, zoom: 3.5})
    const [locations, setLocations] = useState([])
    const [geojson, setGeojson] = useState(null)
    const [size, setSize] = useState({value: "All"}) // initial size state here

    useEffect(() => {
        setLocations(geodata);
        updateLocationData(size.value); // call the location updater on mount with the initial size state value
    }, []);

    const _updateViewport = viewport => {
        setViewport(viewport)
    }

    const _updateData = event => {
        setSize({value: event.target.value})
        updateLocationData(event.target.value); // forward current size value
    }

    const updateLocationData = (sizeValue) => { // forwarded size value
        const tempLocations = [];
        locations.forEach(function(res) {
            if (sizeValue === "All") { // forwarded size value for comparison
                tempLocations.push(res);
            } else if (res.Size === sizeValue) { // forwarded size value for comparison
                tempLocations.push(res);
            }
        });
        var data = {
            ...
        };
        setGeojson(data);
    };

    return (
        <ReactMapGL
            {...viewport}
            onViewportChange={_updateViewport}
            width="100%"
            height="100%"
            mapStyle={mapStyle}
            mapboxApiAccessToken={TOKEN}>
            <Source id="my-data" type="geojson" data={geojson}>
                <Layer {...icon} />
            </Source>
            <div style={navStyle}>
                <NavigationControl onViewportChange={_updateViewport} />
                <select onChange={_updateData} defaultValue={size.value}> // need to unpack the actual size value
                    <option value="All">All</option>
                    <option value="Large">Large</option>
                    <option value="Medium">Medium</option>
                    <option value="Small">Small</option>
                    <option value="Very Small">Very Small</option>
                </select>
            </div>
        </ReactMapGL>
    );
}

export default Map;
HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • that seems to remove the initial component mount error, but it seems to still be 1 step behind, on component mount also. so locations is empty [] and then is stored with the previous selected data option; if I set a const inside updateLocationData, such const currentSize = event.target.value; then the state is correct when selected, however then the initial load does not work as it is undefined – ckingchris Jan 01 '20 at 08:13
  • 1
    @ckingchris Sorry, I see what you mean now, updated my answer with fixes and comments with explanation. Hope it is clearer now. FYI, it would also be a little cleaner to just store the size value directly instead of nested in an object. – Drew Reese Jan 01 '20 at 08:23
  • Let's say the starting defaultValue option of – ckingchris Jan 01 '20 at 08:25
  • no worries, we are close! the updateLocationData is working great now :) It seems that the useEffect updateLocationData(size.value) is still leaving locations empty onload – ckingchris Jan 01 '20 at 08:36
  • What is `geoData` in the effect hook? I don't see where it's defined. The hook is the only place I see `locations` being updated other than the initial value. – Drew Reese Jan 01 '20 at 08:40
  • geodata is a json that loads from import geodata from './geodata'; when I console.log(geodata) before setLocations(geodata); I see the full object...I just added } ,[locations]); at the end of useEffect and it loaded correctly. Thank you for all of your help! – ckingchris Jan 01 '20 at 08:46
0

First of all, useState function is asynchronous. Therefore takes some time to update. See Is useState synchronous?. And second, the useEffect function is called when the component mounts. So no onchange event has occurred yet. Onchange event occurs once you change the option in select tag. Just remove _updateData from your useEffect function.

Prateek Oraon
  • 101
  • 1
  • 8