How to debounce user input for controlled component without lag

484 views Asked by At

I am trying to debounce a call to update codemirror. It kind of "works" but the issue is the editor's value is not updated (at all) until after the debounce call. So if I even press the spacebar, the cursor position does not update until the debounced call has been completed.

The debounced call is an action creator which flows through multiple sagas, reducers and finally updates the component which produces noticeable lag in the app. Is there a way to do this so the user can keep typing without this lag?

EDIT:

this question sums up my issue pretty well: code mirror takes the value from the state, while debounce prevents the state to trigger.

Code:

const cmEditor = ({ selectedFile, updateCode }) => {

  const debounced = useCallback(
    _.debounce((code, selectedFile) => { updateCode({ code, selectedFile }); }, 1000),
    [],
  );

  const update = (editor, data, code) => {
    debounced(code, selectedFile);
  };

  return (
    <Container>
      <CodeMirror
        value={selectedFile.content}
        options={{ ...CODE_MIRROR_DEFAULT_OPTIONS, mode: getCodeMode(EDITORS.CODE_MIRROR.value, selectedFile.mimeType) }}
        onBeforeChange={(editor, data, code) => update(editor, data, code)}
      />
    </Container>
  );
};

cmEditor.propTypes = {
  selectedFile: PropTypes.object.isRequired,
  updateCode: PropTypes.func.isRequired,
};

export default cmEditor;

EDIT 2:

Here is my implementation using local state and an idle timer. Not quite working but almost:

const cmEditor = ({ selectedFile, updateCode }) => {

  const [idleTimer, setIdleTimer] = useState(1);
  const [localCode, setLocalCode] = useState(selectedFile.content);

  useEffect(() => {
    setLocalCode(selectedFile.content);
  }, [selectedFile.content]);

  // Add event listeners
  useEffect(() => {
    window.addEventListener('mousemove', resetIdleTimer);
    window.addEventListener('keypress', resetIdleTimer);

    // Remove event listeners on cleanup
    return () => {
      window.removeEventListener('mousemove', resetIdleTimer);
      window.removeEventListener('keypress', resetIdleTimer);
    };
  }, []); // Empty array ensures that effect is only run on mount and unmount

  useEffect(() => {
    let timer = null;

    timer = setTimeout(() => {
      if (idleTimer === 0) {
          handleUpdate();
          clearTimeout(timer);
          // resetIdleTimer();
        } else {
          setIdleTimer(idleTimer - 1);
        }
    }, 1000);

    return () => clearTimeout(timer);
  }, [idleTimer]);

  const resetIdleTimer = () => setIdleTimer(DEFAULT_IDLE_TIMEOUT);

  const handleUpdate = () => updateCode({ code: localCode, selectedFile });


  return (
    <Container>
      <CodeMirror
        value={localCode}
        options={{ ...CODE_MIRROR_DEFAULT_OPTIONS, mode: getCodeMode(EDITORS.CODE_MIRROR.value, selectedFile.mimeType) }}
        onBeforeChange={(editor, data, code) => setLocalCode(code)}
      />
    </Container>
  );
};
1

There are 1 answers

1
jboot On

I am not too familiar with React, but I assume that the changes will only be visible after the update function completes, which includes the debounce time. You could make the function asynchronous by marking the update function with async, though I don't know if this will have any side effects in your framework:

  const update = async (editor, data, code) => {
    debounced(code, selectedFile);
  };

As an alternative you could consider using the onChange trigger instead of onBeforeChange, as it will first process the user's interaction before executing your statement.