Jay
Jay

Reputation: 1056

React Hooks: old state value persisting in closure

Okay I realize this going to be a super long example, but I am thoroughly baffled by this, so thought I'd both share it with everyone and also try to find a solution.

So I'm trying to make a "tag input" component where you type into it, and whenever you type space it appends that string to a list you pass in. In order to remove one of the "tags", you clear out the content editable area, hit backspace once to "prep" the last tag in the list for removal, and then again to confirm the removal. It makes sense in context, but I've written a stripped down version for the sake of the example. I have the following codesandbox: https://codesandbox.io/s/8q0q3qw60

Now, here's the part that I don't get.

Everything seems to be working entirely as intended, except for the actual removal of the tag. For some reason, I can appropriately "prep" the last tag for removal, however when I click backspace again to confirm, for some reason the state (from a hook) for prepTagForRemoval within the closure that is the keyDown callback for the content editable area never changes. It is always false, but only within the callback! This results in the tag never being deleted appropriately after confirming its deletion.

In order to repro this...

  1. open up code sandbox link
  2. click on the red-outlined box (content-editable div)
  3. type "hello " (without the quotes, so hello followed by a space
  4. hello will move up to the line above that says "Tags:"
  5. click backspace once
  6. Prepped for removal is now true, and "hello" is highlighted in red
  7. click backspace again

Currently, at this point, it should have just deleted "hello" from the "Tags:" row, however the actual behavior is that it simply set prepForRemoval to false, and turns hello black again, without removing "hello" from "Tags:"

Sorry if this is confusing, I'm happy to try to clarify more. I really want to get this example properly working and removing the last tag (or at least calling the onRemove) when the second delete is emitted. Hope someone can lend a hand!

Upvotes: 2

Views: 557

Answers (1)

Ryan Cogswell
Ryan Cogswell

Reputation: 81006

This is a bug in react-contenteditable. Below is its shouldComponentUpdate method. It isn't checking for a change to onKeyDown and since the backspace won't cause any change to the value, it won't re-render so it is using a stale version of your handleKeyDown function.

  shouldComponentUpdate(nextProps: Props): boolean {
    const { props } = this;
    const el = this.getEl();

    // We need not rerender if the change of props simply reflects the user's edits.
    // Rerendering in this case would make the cursor/caret jump

    // Rerender if there is no element yet... (somehow?)
    if (!el) return true;

    // ...or if html really changed... (programmatically, not by user edit)
    if (
      normalizeHtml(nextProps.html) !== normalizeHtml(el.innerHTML)
    ) {
      return true;
    }

    // Handle additional properties
    return props.disabled !== nextProps.disabled ||
      props.tagName !== nextProps.tagName ||
      props.className !== nextProps.className ||
      props.innerRef !== nextProps.innerRef ||
      !deepEqual(props.style, nextProps.style);
  }

Here it is working with a fixed copy of react-contenteditable: https://codesandbox.io/s/o41yjr3r3q

I changed the last part of shouldComponentUpdate to add props.onKeyDown !== nextProps.onKeyDown:

return (
  props.disabled !== nextProps.disabled ||
  props.tagName !== nextProps.tagName ||
  props.className !== nextProps.className ||
  props.innerRef !== nextProps.innerRef ||
  props.onKeyDown !== nextProps.onKeyDown ||
  !deepEqual(props.style, nextProps.style)
);

Upvotes: 2

Related Questions