slate: Focus breaks when calling setState in onFocus / onBlur

Do you want to request a feature or report a bug?

bug

What’s the current behavior?

When you call setState() from within an onFocus() or onBlur() handler the editor can no longer receive / release focus. Here is a JSFiddle illustrating the problem: https://jsfiddle.net/fj9dvhom/2611/. Just try to focus or unfocus the editor.

Chrome: Version 70.0.3538.102 (Official Build) (64-bit) Mac OS: 10.14.1

What’s the expected behavior?

Focusing should work correctly.

I have a suspicion that the setState() call is causing a render event before the editor can finish handling the original focus / blur event. Wrapping the setState() in a setTimeout() seems to alleviate the issue.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 15
  • Comments: 19 (10 by maintainers)

Most upvoted comments

#2451 is a different issue, this bug occurs even with one editor. For some reason setTimeout fixes blurring:

onBlur = (event, editor, next) => {
    next();
    setTimeout(() => this.setState({ hasFocus: false }), 0);
}; 

which imho this is the proof that it is a race condition issue.

See online demo: https://jsfiddle.net/fj9dvhom/2734/

I’m quite surprised to find that setState() on a parent’s onBlur/Focus handler also prevents the editor from gaining/losing focus. I’d expect Editor’s inner workings to be immune from external state changes like this?

I’ve forked @ericedem’s fiddle, and simply moved the onFocus/Blur handler to a <div> wrapper to demonstrate the problem here: https://jsfiddle.net/thatmarvin/sLkq06yt/

Wrapping setTimeout also fixes the problem: https://jsfiddle.net/thatmarvin/fa0encyd/

Yes, this one needs to be fixed on priority as it’s totally a blocker. It took my 2 days to figure out why my editor was not working, as it doesn’t even throw any error.

I’m quite surprised to find that setState() on a parent’s onBlur/Focus handler also prevents the editor from gaining/losing focus. I’d expect Editor’s inner workings to be immune from external state changes like this?

I’ve forked @ericedem’s fiddle, and simply moved the onFocus/Blur handler to a <div> wrapper to demonstrate the problem here: https://jsfiddle.net/thatmarvin/sLkq06yt/

Wrapping setTimeout also fixes the problem: https://jsfiddle.net/thatmarvin/fa0encyd/

I finally figure out how to avoid editor losing focus. My solution is based on @steida recommandation of tracking isFocused. Here is what I did:

  1. Add this hook somewhere into your code:
const useFocusBlurCallback = (onFocus: () => void, onBlur: () => void) => {
  const focusedRef = useRef(false);

  return (focused: boolean) => {
    if (!focusedRef.current && focused) {
      onFocus();
    } else if (focusedRef.current && !focused) {
      onBlur();
    }
    focusedRef.current = focused;
  };
};
  1. In your component, call this hook:
const setEditorFocused = useFocusBlurCallback(
    () => {
        // onFocus
    },
    () => {
        // onBlur
    }
);
  1. When editor changes:
onChange={value => {
    setEditorFocused(ReactEditor.isFocused(editor));
}}

That trick means that you will block onBlur() event handling for all other plugins until the next react render cycle, which could result in some pretty unexpected behavior. Especially since slate is expecting to call all the event handlers synchronously.

The better workaround is to track selection.isFocused changes and never ever use onFocus nor onBlur imho.

@ericedem Thanks Eric, I’m pretty new to both slate and React, so thanks for the advice. Will go with the setTimeout workaround for now.

Sorry, the one I posted in #2451