Function can not read state value correctly

Viewed 237

I have this component in my app where i am trying to implement infinite scroll according to this tutorial.

export const MainJobs = () => {
  const [items, setItems] = useState([]);
  const [ind, setInd] = useState(1);
  const errText = useLang(
    "sagas // Something went wrond // fasa"
  );
  const [element, setElement] = useState(null);
  const incrementState = () => {
    console.log(ind, ind + 1); //logs 1,2
    setInd(ind + 1);
  };
  useEffect(() => {
    const fetchInfo = async () => {
      const res = await fetch(`/api/search/vacancy?page=${ind}`);
      if (res.ok) {
        const data = await res.json();
        setItems(data);
      } else {
        pop({
          icon: "error",
          title: "Oops...",
          text: errText
        });
      }
    };
    fetchInfo();
  }, [ind]);
  useEffect(() => {
    const currentElement = element;
    const currentObservor = observor.current;
    if (currentElement) {
      currentObservor.observe(currentElement);
    }
    return () => {
      if (currentElement) {
        currentObservor.unobserve(currentElement);
      }
    };
  }, [element]);
  const observor = useRef(
    new IntersectionObserver(
      entries => {
        const first = entries[0];
        if (first.isIntersecting) {
          incrementState();
        }
      },
      { threshold: 1 }
    )
  );
  return (
    <div className="mainjobs-container">
      ...rest of markup
      <div className="bottom" ref={setElement}></div>
    </div>
  );
};

It logs 1,2 every time but doesn't increment ind. Also in devtools ing increments to 2 and then stops.

I need to increment ind every time incrementState function is called. P.S i think the problem is about useRef or in Observor.

1 Answers

From the docs:

Why am I seeing stale props or state inside my function?

Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.

It happens that the incrementState is defined inside the useRef and hence only "sees" the inital state.

If you only wanted to fix the single state, this is pretty simple:

const incrementState = () => {
  setInd(prevInd => prevInd + 1);
}

We use the functional version of state setter, so that we don't miss any updates on state.

But what if this handler actually depended on another state variable?

const observer = useRef();

// Adding a placeholder state for demo
const [secondState, setSecondState] = useState();

useEffect(() => {
  
  const incrementState = () => {
    // something to do with second state here
    if(secondState) 
      setInd(prevInd => prevInd + 1);
  };

  observer.current = new IntersectionObserver(
      entries => {
        const first = entries[0];
        if (first.isIntersecting) {
          incrementState();
        }
      },
      { threshold: 1 }
    );

}, [secondState]);  // note dependency here, ensures that each updates "sees" the new version of function

useEffect(() => {
  // observer and so on...
});

const { Component, useRef, useState, useEffect } = React;
const { render } = ReactDOM;

function App() {
  const thingRef = useRef();
  const [count, setCount] = useState(0);

  const incrementCounter = () => {
    setCount(prevCount => prevCount + 1);
  };

  const observer = useRef(
    new IntersectionObserver(
      entries => {
        const first = entries[0];
        if (first.isIntersecting) {
          incrementCounter();
        }
      },
      { threshold: 1 }
    )
  );

  useEffect(() => {
    const currentElement = thingRef.current;
    const currentObserver = observer.current;
    if (currentElement) {
      currentObserver.observe(currentElement);
    }
    return () => {
      if (currentElement) {
        currentObserver.unobserve(currentElement);
      }
    };
  }, []);

  return (
    <main style={{ height: "600vh", position: "relative" }}>
      <div
        ref={thingRef}
        style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          transform: "translate(-50%, -50%)",
          height: '80vh',
          border: '1px solid pink',
          width: '98%',
          textAlign: 'center',
        }}
      >
        Thing
      </div>
      <p
        style={{
          backgroundColor: 'red',
          borderRadius: '50%',
          color: 'white',
          width: '20px',
          height: '20px',
          lineHeight: '20px',
          textAlign: 'center',
          position: 'fixed',
          top: '10px',
          right: '80px',
          margin: 0
        }}
      >{count}</p>
    </main>
  );
}

render(<App />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root" />

Related