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.

Issue with hooks and onBlur

See original GitHub issue

Hey, very useful component, thanks!

I ran into an issue when trying to use this component in a functional component combined with useState. The issue is that not all prop changes lead to an update of the component, therefore an onBlur like in my example would return the old initial content value.

The issue resides in the current implementation of shouldComponentUpdate.

Please look at this example Codesandbox. I copied this component’s current source code over there and just return true in the shouldComponentUpdate and everything works fine. To see the issue, comment the return true and uncomment the original code. If you type something and look in the console, you’ll see the following logs:

render log: 
render log: a
render log: as
render log: asd
onBlur log: 

To fix this, I’d suggest going with a return true or make it a PureComponent to make updates based on prop changes.

Maintainer edit

Short answer

Do this

  const text = useRef('');

  const handleChange = evt => {
    text.current = evt.target.value;
  };

  const handleBlur = () => {
    console.log(text.current);
  };

  return <ContentEditable
      html={text.current}
      onBlur={handleBlur}
      onChange={handleChange} />

NOT THAT

  const [text, setText] = useState('');

  const handleChange = evt => {
    setText(evt.target.value);
  };

  const handleBlur = () => {
    console.log(text); // incorrect value
  };

  return <ContentEditable
      html={text}
      onBlur={handleBlur}
      onChange={handleChange} />

Explanation

react-contenteditable has to prevent rendering (using shouldComponentUpdate) very frequently. Otherwise, the caret would jump to the end of the editable element on every key stroke. With useState, you create a new onBlur event handler on every keystroke, but since the ContentEditable is preventing rendering, your event handlers are not taken into account on every keystroke, and it’s the handler function that was creating the last time the component was rendered that gets called.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:40
  • Comments:21 (5 by maintainers)

github_iconTop GitHub Comments

58reactions
ctrlplusbcommented, Aug 6, 2020

Hey all;

So I also hit this issue. It appears to be less about the “hooks” feature of React, and more about the paradigm that “hooks” operate within - i.e. the need to create new callback functions containing new scope closures.

The current design of this component, along with the usage of shouldComponentUpdate does not play nicely with this paradigm. I haven’t gone into the source in depth, and understand the introduction of shouldComponentUpdate was to prevent the caret position from changing. However, the current design doesn’t allow new instances of callback components to be passed down, therefore you can get into a case where an old callback instance is called with a stale closure scope, leading to difficult to debug issues and strange behaviour.

To try and alleviate this I created my own wrapping component which manages refs of each callback and then passes down the callbacks down.

I’ve had good results from this as a temporary measure. My longer term hope is that we can revisit the internal design of this component as a community and move it forward.

Here is my wrapping component (click to view)
// wrapped-content-editable.js

import React from 'react';
import ReactContentEditable from 'react-contenteditable';

export default function ContentEditable({
  onChange,
  onInput,
  onBlur,
  onKeyPress,
  onKeyDown,
  ...props
}) {
  const onChangeRef = React.useRef(onChange);
  const onInputRef = React.useRef(onInput);
  const onBlurRef = React.useRef(onBlur);
  const onKeyPressRef = React.useRef(onKeyPress);
  const onKeyDownRef = React.useRef(onKeyDown);

  React.useEffect(() => {
    onChangeRef.current = onChange;
  }, [onChange]);
  React.useEffect(() => {
    onInputRef.current = onInput;
  }, [onInput]);
  React.useEffect(() => {
    onBlurRef.current = onBlur;
  }, [onBlur]);
  React.useEffect(() => {
    onKeyPressRef.current = onKeyPress;
  }, [onKeyPress]);
  React.useEffect(() => {
    onKeyDownRef.current = onKeyDown;
  }, [onKeyDown]);

  return (
    <ReactContentEditable
      {...props}
      onChange={
        onChange
          ? (...args) => {
              if (onChangeRef.current) {
                onChangeRef.current(...args);
              }
            }
          : undefined
      }
      onInput={
        onInput
          ? (...args) => {
              if (onInputRef.current) {
                onInputRef.current(...args);
              }
            }
          : undefined
      }
      onBlur={
        onBlur
          ? (...args) => {
              if (onBlurRef.current) {
                onBlurRef.current(...args);
              }
            }
          : undefined
      }
      onKeyPress={
        onKeyPress
          ? (...args) => {
              if (onKeyPressRef.current) {
                onKeyPressRef.current(...args);
              }
            }
          : undefined
      }
      onKeyDown={
        onKeyDown
          ? (...args) => {
              if (onKeyDownRef.current) {
                onKeyDownRef.current(...args);
              }
            }
          : undefined
      }
    />
  );
}

Yeah, I know that looks a bit verbose and repetitive, but I prefer the explicit simplicity. 😊

You can now write your code as described how not to do it in within the OP.

i.e. This is fine now:

function Demo() {
  const [text, setText] = React.useState('Woot! Hooks working');

  const handleChange = evt => {
    setText(evt.target.value);
  };

  const handleBlur = () => {
    console.log(text); // 👍 correct value
  };

  return (
    <ContentEditable html={text} onBlur={handleBlur} onChange={handleChange} />
  );
}

Demo here

35reactions
rasendubicommented, Nov 30, 2020

You can use the following hook to reduce boilerplate

const useRefCallback = <T extends any[]>(
  value: ((...args: T) => void) | undefined,
  deps?: React.DependencyList
): ((...args: T) => void) => {
  const ref = React.useRef(value);

  React.useEffect(() => {
    ref.current = value;
  }, deps ?? [value]);

  const result = React.useCallback((...args: T) => {
    ref.current?.(...args);
  }, []);

  return result;
};
Usage in the wrapping component
import React from 'react';
import ReactContentEditable, { Props } from 'react-contenteditable';

const useRefCallback = <T extends any[]>(
  value: ((...args: T) => void) | undefined,
  deps?: React.DependencyList
): ((...args: T) => void) => {
  const ref = React.useRef(value);

  React.useEffect(() => {
    ref.current = value;
  }, deps ?? [value]);

  const result = React.useCallback((...args: T) => {
    ref.current?.(...args);
  }, []);

  return result;
};

export default function ContentEditable({
  ref,
  onChange,
  onInput,
  onBlur,
  onKeyPress,
  onKeyDown,
  ...props
}: Props) {
  const onChangeRef = useRefCallback(onChange);
  const onInputRef = useRefCallback(onInput);
  const onBlurRef = useRefCallback(onBlur);
  const onKeyPressRef = useRefCallback(onKeyPress);
  const onKeyDownRef = useRefCallback(onKeyDown);

  return (
    <ReactContentEditable
      {...props}
      onChange={onChangeRef}
      onInput={onInputRef}
      onBlur={onBlurRef}
      onKeyPress={onKeyPressRef}
      onKeyDown={onKeyDownRef}
    />
  );
}

If you don’t want to copy the wrapping component, you can use useRefCallback as a drop-in replacement for useCallback:

function Demo() {
  const [text, setText] = React.useState('Woot! Hooks working');

  const handleChange = useRefCallback((evt) => {
    setText(evt.target.value);
  }, []);

  const handleBlur = useRefCallback(() => {
    console.log(text); // 👍 correct value
  }, [text]);

  return (
    <ContentEditable html={text} onBlur={handleBlur} onChange={handleChange} />
  );
}

Demo

Read more comments on GitHub >

github_iconTop Results From Across the Web

reactjs - React hook - onFocus and onBlur - Stack Overflow
Basically, how this works is, the useState hook returns an array of two things, the first one is the default value to the...
Read more >
react hook form onblur not working - You.com | The AI Search ...
In your code, the form mode is onBlur . it means the validation is triggered on blur event (unfocus the input). When you...
Read more >
useForm | React Hook Form - Simple React forms validation
useForm is a custom hook for managing forms with ease. ... Note: when using with Controller , make sure to wire up onBlur...
Read more >
React Hook Form - Validation onBlur - CodeSandbox
React Hook Form - Validation onBlur. 1. Embed Fork Create Sandbox Sign in. Sandbox Info. React Hook Form - Validation onBlur. validation. react-hooks....
Read more >
React Hook Form: A guide with examples - LogRocket Blog
If you want to validate the field when there is an onChange or onBlur event, you can pass a mode property to the...
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