Search code examples
reactjsreact-hooksreact-statereact-map-gl

React useState option value 1 step behind onChange selection


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;

Solution

  • 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;