Search code examples
reactjsuse-effectuse-state

react function component issue with usEffect and useState


Sometimes I have to use some native js libaray api, So I may have a component like this:

function App() {
  const [state, setState] = useState(0)
  useEffect(() => {
    const container = document.querySelector('#container')
    const h1 = document.createElement('h1')
    h1.innerHTML = 'h1h1h1h1h1h1'
    container.append(h1)
    h1.onclick = () => {
      console.log(state)
    }
  }, [])
  return (
    <div>
      <button onClick={() => setState(state => state + 1)}>{state}</button>
      <div id="container"></div>
    </div>
  )
}

Above is a simple example. I should init the lib after react is mounted, and bind some event handlers. And the problem is coming here: As the above shown, if I use useEffect() without state as the item in dependencies array, the value state in handler of onclick may never change. But if I add state to dependencies array, the effect function will execute every time once state changed. Above is a easy example, but the initialization of real library may be very expensive, so that way is out of the question.

Now I find 3 ways to reslove this, but none of them satisfy me.

  1. Create a ref to keep state, and add a effect to change it current every time once state changed. (A extra variable and effect)
  2. Like the first, but define a variable out of the function instead of a ref. (Some as the first)
  3. Use class component. (Too many this)

So is there some resolutions that solve problems and makes code better?


Solution

  • I think you've summarised the options pretty well. There's only one option i'd like to add, which is that you could split your code up into one effect that initializes, and one effect that just changes the onclick. The initialization logic can run just once, and the onclick can run every render:

    const [state, setState] = useState(0)
    const h1Ref = useRef();
    useEffect(() => {
      const container = document.querySelector('#container')
      const h1 = document.createElement('h1')
      h1Ref.current = h1;
      // Do expensive initialization logic here
    }, [])
    useEffect(() => {
      // If you don't want to use a ref, you could also have the second effect query the dom to find the h1
      h1ref.current.onClick = () => {
        console.log(state);
      }
    }, [state]);
    

    Also, you can simplify your option #1 a bit. You don't need to create a useEffect to change ref.current, you can just do that in the body of the component:

    const [state, setState] = useState(0);
    const ref = useRef();
    ref.current = state;
    useEffect(() => {
      const container = document.querySelector('#container');
      // ...
      h1.onClick = () => {
        console.log(ref.current);
      }
    }, []);