简体   繁体   中英

React useEffect state variable console.logs different values consecutively

I am currently writing a map component using Mapbox. But I encounter an error on React hooks during development.

In useEffect state variable that is declared prints two different values.

As i explain in the below code. startDrawing is console.logs both true and false after second click on <IconButton/> button.

import React from "react";
import mapboxgl from "mapbox-gl";
import { Add, Delete } from "@material-ui/icons";
import IconButton from "@material-ui/core/IconButton";

export default function MapComponent() {
  const mapContainerRef = React.useRef(null);

  const [startDrawing, setStartDrawing] = React.useState(false);
  const [map, setMap] = React.useState(null);

  const initMap = () => {
    mapboxgl.accessToken = "mapbox-token";

    const mapbox = new mapboxgl.Map({
      container: mapContainerRef.current,
      style: "mapbox://styles/mapbox/streets-v11",
      center: [0, 0],
      zoom: 12,
    });

    setMap(mapbox);
  };

  React.useEffect(() => {
    if (!map) {
      initMap();
    } else {
      map.on("click", function (e) {
        
        //  After second click on set drawing mode buttons
        //   startDrawing value writes two values for each map click

        // MapComponent.js:85 true
        // MapComponent.js:85 false

        // MapComponent.js:85 true
        // MapComponent.js:85 false

        // MapComponent.js:85 true
        // MapComponent.js:85 false

        // MapComponent.js:85 true
        // MapComponent.js:85 false

        // MapComponent.js:85 true
        // MapComponent.js:85 false

        console.log(startDrawing);
        if (startDrawing) {
          // do stuff
        } else {
          // do stuff
        }
      });
    }
  }, [map, startDrawing]);

  return (
    <>
      <div>
        {/*  set drawing mode */}
        <IconButton onClick={() => setStartDrawing(!startDrawing)}>
          {startDrawing ? <Delete /> : <Add />}
        </IconButton>
      </div>

      <div ref={mapContainerRef} />
    </>
  );
}

So my question is how can i solve this problem?

Thank you for your answers.

The issue is that useEffect will trigger both on mount and unmount (render and destroy). Refer to this documentation for a detailed explanation.

To run the function only on the first render , you can pass an empty array as the second parameter of useEffect , just like this:

useEffect(()=>{
    //do stuff
},[]); // <--- Look at this parameter

The last parameter serves as a flag and usually a state should be passed, which will make useEffect 's function trigger only if the parameter's value is different from the previous.

Let's assume you want to trigger useEffect each and every time your state.map changes - you cold do the following:

const [map, setMap] = React.useState(null);
useEffect(()=>{
   //do stuff
},map); // if map is not different from previous value, function won't trigger

The issue is that you are adding a new event listener to map every time startDrawing changes. When you click on the rendered element all of those listeners are going to be fired, meaning you get every state of startDrawing the component has seen.

See this slightly more generic example of your code, and note that every time you click Add or Delete a new event listener gets added to the target element:

 const { useState, useRef, useEffect } = React; function App() { const targetEl = useRef(null); const [startDrawing, setStartDrawing] = useState(false); const [map, setMap] = useState(null); const initMap = () => { setMap(targetEl.current); }; useEffect(() => { if (;map) { initMap(). } else { const log = () => console;log(startDrawing). map,addEventListener('click'; log), } }, [map; startDrawing])? return ( <div> <div> <button onClick={() => setStartDrawing(:startDrawing)}> {startDrawing; <span>Delete</span>. <span>Add</span>} </button> </div> <div ref={targetEl}>target element</div> </div> ), } ReactDOM.render(<App/>; document.getElementById('root'));
 <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <div id="root"></div>

You can fix this by adding a return statement to your useEffect. This is triggered immediately before the effect updates with new values from the dependency array, and also when the component unmounts. Inside the return statement you should remove the previous event listener so that only one is attached to the element at any given point. For the above example it would look like this:

useEffect(() => {
  if (!map) {
    initMap();
  } else {
    const log = () => console.log(startDrawing);
    map.addEventListener('click', log);
    return () => {
      map.removeEventListener('click', log);
    };
  };
}, [map, startDrawing]);

Ideally you would not use the standard JS event syntax at all, as the convention in React is to attach events declaratively in the return/render function so that they can always reference the current state. However, you are using an external library, and I don't know whether it has any explicit support for React - you should probably check that out.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM