question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

No stable way to get current state of component when using hooks

See original GitHub issue

Because the identity of the value returned by returned by the useState hook is not stable between renders, some tasks that were easy with class components are challenging or impossible with hooks.

For example, here is a simplified example of a component that can be clicked and dragged, written first as a class component and second as a functional component with hooks:

class DraggableClass extends React.Component {
  state = {
    isDragging: false,
    position: [0, 0],
  };

  handleMouseMove = event => {
    if (this.state.isDragging) {
      const newX = this.state.position[0] + event.movementX;
      const newY = this.state.position[1] + event.movementY;
      this.setState({ position: [newX, newY] });
    }
  };

  handleMouseUp = () => {
    this.setState({ isDragging: false });
  };

  componentDidMount() {
    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('mouseup', handleMouseUp);
  }

  componentWillUnmount() {
    window.removeEventListener('mousemove', handleMouseMove);
    window.removeEventListener('mouseup', handleMouseUp);
  }

  render() {
    return (
      <div
        style={{
          position: 'absolute',
          left: this.state.position[0],
          top: this.state.position[1],
        }}
        onMouseDown={ () => this.setState({ isDragging: true }) }
      >
        Drag me!
      </div>
    );
  }
}

function DraggableHooks() {
  const [isDragging, setIsDragging] = useState(false);
  const [position, setPosition] = useState([0, 0]);

  useEffect(() => {
    function handleMouseMove(event) {
      if (isDragging) {
        const newX = position[0] + event.movementX;
        const newY = position[1] + event.movementY;
        setPosition([newX, newY]);
      }
    }
    window.addEventListener('mousemove', handleMouseMove);

    function handleMouseUp() {
      setIsDragging(false);
    }
    window.addEventListener('mouseup', handleMouseUp);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp);
    }
  }, []);

  return (
    <div
      style={{
        position: 'absolute',
        left: position[0],
        top: position[1],
      }}
      onMouseDown={ () => setIsDragging(true) }
    >
      Drag me!
    </div>
  );
}

The hooks version does not work because the isDragging and position variables within handleMouseMove are stuck with the old identities of both variables, and never see any new values. The observed behavior in this case is that if (isDragging) always evaluates to false, even though the value of isDragging in state does change.

I suspect that the fix for this isn’t as simple as “just make the state value stable like setState”, since I’m guessing that would mess with async rendering. But perhaps something like the following could help:

// This advanced hook would have a stable identity for both value and setValue.
const [value, setValue] = useStableState(defaultValue);

// This would add a third return value to useState which has a stable identity.
const [value, setValue, getValue] = useState(defaultValue);

I am currently using something like the latter in my own code, using refs to persist values for me:

export function useStableState(defaultValue) {
  const [value, setValue] = useState(defaultValue);
  const valueRef = useRef(defaultValue);

  return [
    value,
    newValue => {
      if (typeof newValue === 'function') {
        newValue = newValue(valueRef.current);
      }

      valueRef.current = newValue;
      setValue(newValue);
    },
    () => valueRef.current,
  ];
}

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:1
  • Comments:6 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
brettinternetcommented, Jul 8, 2020

For future readers, I think it’s worth pointing out that OP’s example is missing dependencies in the useEffect implementation which is why it didn’t work. I’m unconvinced that useRef in this example is any better than useState. Thank you for the discussion, OP. I’ve consolidated the 3 examples here: https://codesandbox.io/s/react-state-classes-usestate-useref-moyuz

1reaction
bvisnesscommented, Mar 26, 2019

Oh interesting, I hadn’t considered doing things that way. This still feels to me like a sharp edge that could cause a lot of frustration - maybe at the very least that FAQ could be updated to include an example of the pattern you showed me.

Thanks for looking into this!

Read more comments on GitHub >

github_iconTop Results From Across the Web

How To Manage State with Hooks on React Components
In this tutorial, you'll manage state on functional components using a method encouraged by the official React documentation: Hooks.
Read more >
Hooks FAQ - React
Hooks are a more direct way to use the React features you already know — such as state, lifecycle, context, and refs. They...
Read more >
How can I force a component to re-render with hooks in React?
useState returns a pair of values: the current state and a function that updates it - state and setter, here we are using...
Read more >
React.useEffect Hook – Common Problems and How to Fix ...
The reason our component is re-rendering is because our useEffect dependency is constantly changing. But why? We are always passing the same ...
Read more >
The last guide to the useEffect Hook you'll ever need
With useEffect , you invoke side effects from within functional components, which is an important concept to understand in the React Hooks era....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found