matskraft
matskraft

Reputation: 23

React useState is reset to undefined and I have no idea why

I am still very new to react and I ran into the following problem:
I created a component which will contains a watchPosition method and an interval. This component will get re-rendered very often but I don't want the interval and the watchPosition method to get re-created every time. My solution is to store the ids of the interval and watchPosition in useState so that a new interval/watchPosition only gets created if the states are undefined. This is almost working :D
This is my code (I added some console outputs to see what's happening):

import { useState } from "react";
import "./styles.css";

export default function App() {
  console.log("NEW RENDER");
  
  const [watcherId, setWatcherId] = useState();
  const [updatingInterval, setUpdatingInterval] = useState();
  //test is a dummy variable to manually create re-rendering and check if new intervals get created
  const [test, setTest] = useState(1);

  async function initializeContext() {
    console.log("current value of updatingInterval: ", updatingInterval);
    if (updatingInterval === undefined) {
      console.log("new interval created");
      setUpdatingInterval(setInterval(updateValues, 1000));
    }
    console.log("current value of watcherId: ", watcherId);
    if (watcherId === undefined) {
      console.log("new geoWatcher created");
      setWatcherId(navigator.geolocation.watchPosition(success, error, {
        maximumAge: 500,
        enableHighAccuracy: true
      }));
    }
  }

  function success() {
    console.log("executed!");
  }

  function error(error) {
    console.error("error: ", error);
  }

  function buttonFunction() {
    setTest((prevText) => {
      return prevText + 1;
    });
  }

  function updateValues() {
    console.log("Interval executed");
  }

  initializeContext();
  return (
    <div className="App">
      <h1>{test}</h1>
      <button onClick={buttonFunction}>Switch</button>
    </div>
  );
}

This is the corresponding console log: enter image description here

As you can see for some reason the values of the two states are set to 16 and 1 and in the next execution they are undefined again. After that they are set to 25 and 2 and from this time on everything works as expected.
I have no idea what I did wrong.

Upvotes: 1

Views: 964

Answers (3)

Nick Vu
Nick Vu

Reputation: 15520

You should not call states directly into the rendering function. That may cause infinite re-renderings. For example, you update states > related components get re-rendered > states will be updated again (because state update will be called in the rendering again).

According to your logic, seemingly, you want to update states only once after the component App gets rendered. In that case, I'd suggest you use useEffect instead.

Furthermore, you should use useRef for the interval and watcherId. That would help you to reduce useless renderings.

import { useState, useEffect, useRef } from "react";
import "./styles.css";

export default function App() {
  console.log("NEW RENDER");
  
  const watcherIdRef = useRef();
  const updatingIntervalRef = useRef();
  //test is a dummy variable to manually create re-rendering and check if new intervals get created
  const [test, setTest] = useState(1);

  //only called in the first rendering
  useEffect(() => {
    initializeContext();
  }, [])

  function initializeContext() {
    //clean up the interval
    if(updatingIntervalRef.current) {
       clearInterval(updatingIntervalRef.current)
    }
    
    //no need to have undefined check
    updatingIntervalRef.current = setInterval(updateValues, 1000);
    watcherIdRef.current = navigator.geolocation.watchPosition(success, error, {
        maximumAge: 500,
        enableHighAccuracy: true
    });
  }

  function success() {
    console.log("executed!");
  }

  function error(error) {
    console.error("error: ", error);
  }

  function buttonFunction() {
    setTest((prevText) => {
      return prevText + 1;
    });
  }

  function updateValues() {
    console.log("Interval executed");
  }

  return (
    <div className="App">
      <h1>{test}</h1>
      <button onClick={buttonFunction}>Switch</button>
    </div>
  );
}

Testable version

const { useState, useEffect, useRef } = React;

function App() {
  console.log("NEW RENDER");
  
  const watcherIdRef = useRef();
  const updatingIntervalRef = useRef();
  //test is a dummy variable to manually create re-rendering and check if new intervals get created
  const [test, setTest] = useState(1);

  //only called in the first rendering
  useEffect(() => {
    initializeContext();
  }, []);

  function initializeContext() {
    console.log("current value of updatingInterval: ", updatingIntervalRef.current);
    //clean up the interval
    if(updatingIntervalRef.current) {
       clearInterval(updatingIntervalRef.current)
    }
    
    //no need to have undefined check
    updatingIntervalRef.current = setInterval(updateValues, 1000);
    watcherIdRef.current = navigator.geolocation.watchPosition(success, error, {
        maximumAge: 500,
        enableHighAccuracy: true
    });
  }

  function success() {
    console.log("executed!");
  }

  function error(error) {
    console.error("error: ", error);
  }

  function buttonFunction() {
    setTest((prevText) => {
      return prevText + 1;
    });
  }

  function updateValues() {
    console.log("Interval executed");
  }

  return (
    <div className="App">
      <h1>{test}</h1>
      <button onClick={buttonFunction}>Switch</button>
    </div>
  );
}

ReactDOM.render(
  <App/>,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Upvotes: 1

Kishor Kumar Mali
Kishor Kumar Mali

Reputation: 26

    I think you should use useEffect and provide a blank dependency array for calling initializeContext 
    
    import { useState ,useEffect} from "react";
    import "./styles.css";
    
    export default function App() {
      console.log("NEW RENDER");
      
      const [watcherId, setWatcherId] = useState();
      const [updatingInterval, setUpdatingInterval] = useState();
      //test is a dummy variable to manually create re-rendering and check if new intervals get created
      const [test, setTest] = useState(1);
    
      async function initializeContext() {
        console.log("current value of updatingInterval: ", updatingInterval);
        if (updatingInterval === undefined) {
          console.log("new interval created");
          setUpdatingInterval(setInterval(updateValues, 1000));
        }
        console.log("current value of watcherId: ", watcherId);
        if (watcherId === undefined) {
          console.log("new geoWatcher created");
          setWatcherId(navigator.geolocation.watchPosition(success, error, {
            maximumAge: 500,
            enableHighAccuracy: true
          }));
        }
      }
    
      function success() {
        console.log("executed!");
      }
    
      function error(error) {
        console.error("error: ", error);
      }
    
      function buttonFunction() {
        setTest((prevText) => {
          return prevText + 1;
        });
      }
    
      function updateValues() {
        console.log("Interval executed");
      }
     useEffect(()=>{
      initializeContext();
    },[])
      return (
        <div className="App">
          <h1>{test}</h1>
          <button onClick={buttonFunction}>Switch</button>
        </div>
      );
    }

//If any issues still Please ask.

Upvotes: 0

Hans Murangaza DRCongo
Hans Murangaza DRCongo

Reputation: 860

remove initializeContext() from function ,You should call this in useEffect

useEffect(()=>{
  initializeContext();
}, [])

Not in function component. When it prints values it is when the setIterval is executing the function, and it prints undefined when the function is not called (meanwhile)

Upvotes: 0

Related Questions