1

So I have this bit of code that does not work as expected. Current focus has been set using useState() on the parent component, hence its a state variable. However, when the currentFocus value changes in the parent, focus variable here itself is not updated. I would have expected the re render of the parent component, which in turn rerenders this component would cause the foucs value to change.

import React, { useRef, useEffect, useState } from 'react';
const CookieDetails = props => {
  const {
    name,
    cost,
    value,
    numOwned,
    onMouseClick,
    cookieId,
    currentFocus,
  } = props;

  let cookieInfoRef = useRef(null);
  //My focus doesnt change even if I change currentFocus in parent component
  const [focus, setFocus] = useState(currentFocus);

  useEffect(() => {
    console.log('currentFocus', currentFocus);
    console.log('focus', focus);
    console.log('cookieID', cookieId); 
    if (cookieInfoRef.current && cookieId === focus) {
      console.log('current', cookieInfoRef.current);
      cookieInfoRef.current.focus();
    }
  }, [focus]);

  return (
    <React.Fragment>
      <button
        onClick={onMouseClick}
        ref={cookieId === focus ? cookieInfoRef : null}
      >
        <h3>{name}</h3>
        <p>Cost:{cost}</p>
        <p>Value:{value}</p>
        <p>Owned:{numOwned}</p>
      </button>
    </React.Fragment>
  );
};

export default CookieDetails;

Now I can solve this problem by doing the following instead,

import React, { useRef, useEffect, useState } from 'react';
const CookieDetails = props => {
  const {
    name,
    cost,
    value,
    numOwned,
    onMouseClick,
    cookieId,
    currentFocus,
  } = props;

  let cookieInfoRef = useRef(null);

  useEffect(() => {
    console.log('currentFocus', currentFocus);
    console.log('cookieID', cookieId);        
    if (cookieInfoRef.current && cookieId === currentFocus) {
      console.log('current', cookieInfoRef.current);
      cookieInfoRef.current.focus();
    }
  });

  return (
    <React.Fragment>
      <button
        onClick={onMouseClick}
        ref={cookieId === currentFocus ? cookieInfoRef : null}
      >
        <h3>{name}</h3>
        <p>Cost:{cost}</p>
        <p>Value:{value}</p>
        <p>Owned:{numOwned}</p>
      </button>
    </React.Fragment>
  );
};

export default CookieDetails;

But I only wanted to run the useEffect hook when focus/currentFocus is updated and not after every render. So why does this happen? What am I am missing to understand here.

Also I previously noticed if you did something like this, the handleStuff function always uses the initial value of 100 (so its always 100 + 10) instead of incrementing by 10 each time a key is pressed. I can solve this by removing the empty [] as the second argument in useEffect. However, I expected the handleStuff to still update with the latest value instead, given that the eventListner has already been added on initial render and on second keydown it should add 10 to 110 instead of 100, but it keeps using the initial state value of 100 instead. Why must you clear the old listener and add the new one each time for it work?

[value, setValue] = useState(100)

handleStuff = (event) => {
 setValue(value+10)
}

useEffect(() => {
  window.addEventListener('keydown', handleStuff)
  return () => {
    window.removeEventListener('keydown', handleStuff)
  }
 },[]);

I am not really insterested in the solutions, I am really insterested in understanding how useEffect() and useState() hooks function in this conditions.

ssaquif
  • 300
  • 2
  • 13
  • We remove the event listener each time because otherwise you’ll have many listeners instead of one. – evolutionxbox Mar 11 '20 at 02:12
  • Try understanding the functions without focus first as I think that is over complicating things – evolutionxbox Mar 11 '20 at 02:14
  • @evolutionxbox why would there be multiple listeners, bu using [] as the second argument, I am only adding one event listener. The problem seems to be that event listener however always uses that very first initial state with which it was rendered. I don't think using a simpler example would help here. As I am particularly interested in this cases. I usually dont have a problem using the hooks, I already provided the solutions to this problems. It's the explanation of this certain behavior that I am looking for – ssaquif Mar 11 '20 at 02:30
  • Where do you change `focus` state, I don't see you call function `setFocus` to change it ? – dungmidside Mar 11 '20 at 02:33
  • 1
    `The problem seems to be that event listener however always uses that very first initial state with which it was rendered` this is called a closure – Thomas Mar 11 '20 at 02:36
  • I do not, since initially I am setting it using useState(currentFocus). currentFocus is a state variable from the parent component ie. [[currentFocus,setCurrentFocus} = useState('someInitialValue')] passed down as prop, so when that changes shouldn't that cause focus to change too, even if I don't call setFocus in the child? – ssaquif Mar 11 '20 at 02:39
  • 1
    It's *some **Initial** Value*. If it would behave the way you expect, the `useState` hook would be useless. It would produce this: `const value = initialValue;` And `setValue` would be pointless because by the next call, `value` would be reset to the initial value. – Thomas Mar 11 '20 at 02:46
  • @Thomas, thank you this is the answer I was looking for. It seems so obvious now, of course it's a closure and hence state is preserved. Thank you – ssaquif Mar 11 '20 at 04:01

1 Answers1

6

Hey the useEffect hook can be a bit confusing. But the way to think of it is

useEffect(() => {
  // Called in an infinite loop (most cases you won't need this)
})

useEffect(() => {
  // Called on first render
}, [])

useEffect(() => {
  // Called when x y & z updates
}, [x, y, z])

The problem is you aren't listening for the currentFocus update in your code.

useEffect(() => {
  // Called when focus updates
}, [focus]);

useEffect(() => {
  // Called when props.currentFocus & focus updates
}, [props.currentFocus, focus]);

Now to explain your confusion:

// My focus doesnt change even if I change currentFocus in parent component
const [focus, setFocus] = useState(currentFocus);

When you are passing in currentFocus here the only time the currentFocus value is being passed to the focus useState is on the first render. So you need to listen for the currentFocus update and then set the new focus which should work how you want!

// Focus set to props.currentFocus on first render
const [focus, setFocus] = useState(currentFocus);

useEffect(() => {
  // Update focus
  setFocus(props.currentFocus)
}, [props.currentFocus])

useEffect(() => {
  // currentFocus will be the same as focus now
  console.log('currentFocus', currentFocus);
  console.log('focus', focus);
  console.log('cookieID', cookieId); 
  if (cookieInfoRef.current && cookieId === focus) {
    console.log('current', cookieInfoRef.current);
    cookieInfoRef.current.focus();
  }
}, [focus]);

Hope that helps!

Alex Dunlop
  • 1,383
  • 15
  • 24