Skip to content

onEditorChange triggers on first render #587

Description

What is the current behavior?

The onEditorChange callback fires immidiatelly on the first render. I'm not sure if thats expected but I would expect to not fire since there was no actual change to the content.

Same happens with either the value or initialValue property.

Please provide the steps to reproduce and if possible a minimal demo of the problem via codesandbox.io or similar.

https://codesandbox.io/p/devbox/vibrant-wilson-ms4v5p

What is the expected behavior?

onEditorChange should not fire

Which versions of TinyMCE, and which browser / OS are affected by this issue? Did this work in previous versions of TinyMCE or tinymce-react?

version 4.3.2

Activity

  1. Mario-Eis commented on Apr 25, 2025

    @Mario-Eis

    I think this might be related to #298
    In a nutshell: you have to provide the exact markup that tinyMCE expects. Otherwise it will normalize to markup, fire a change event and gets dirty. And there is no possibility to detect weather that was triggered by a user input or by an automatic normalization.
    That's a huge, very critical issue when using the editor with forms that leaves the controlled editor almost useless. Still fighting that bug on almost every release.

  2. carlosmintfan commented on May 5, 2025

    @carlosmintfan

    @Mario-Eis Could you elaborate why this is so critical? Is it not possible for you to make sure you only pass normalized HTML into the value/initialValue?

  3. carlosmintfan commented on May 5, 2025

    @carlosmintfan

    Like by getting the normalized value from TinyMCE manually and only passing that in the future?

  4. carlosmintfan commented on May 6, 2025

    @carlosmintfan

    Well, can’t you do that in some testing project where it doesn’t matter if the change event fires just to get the normalized value? I‘d like to get a deeper understanding of the problem!

  5. carlosmintfan commented on May 6, 2025

    @carlosmintfan

    Is your initial value not hardcoded into your application but fetched dynamically from some database? Yes, then it gets more complicated if it doesn’t originate from TinyMCE output

  6. Mario-Eis commented on Jul 24, 2025

    @Mario-Eis

    @Mario-Eis Could you elaborate why this is so critical? Is it not possible for you to make sure you only pass normalized HTML into the value/initialValue?

    @carlosmintfan Exactly. In our case, we fetch the data from a database and use tinymce-react in a form. react-hook-form in our case. TinyMCE is one input field of many. And permanently sets the form to dirty when it is not. In some cases the data in that field is created by sources that are not tinymce. But the HTML is still perfectly valid and renders perfectly fine. So normalizing it when the first user input is made and the form legally gets dirty would be the desired behavior. Instead of setting the form to dirty automatically without user input.

  7. szado commented on Sep 26, 2025

    @szado

    Same issue here.

  8. Dionnie commented on Oct 4, 2025

    @Dionnie

    PLEEEEEEEEEEEEEASE!!! Fix this!!! I will lose my job!!!

  9. carlosmintfan commented on Oct 4, 2025

    @carlosmintfan

    @Mario-Eis @szado @Dionnie Digging into this now (although I am not a Tiny employee); are you giving the initial value from the form lib to the editor synchronously or asynchronously?
    Because if you're doing it synchronously there should be a workaround: #298 (comment)
    The problem (for which I may have another workaround, I'll have to test) is setting the initial content asynchronously: #298 (comment)

  10. carlosmintfan commented on Oct 4, 2025

    @carlosmintfan
    import { useRef } from 'react';
    
    function EditorWrapper(props) {
      const hasInitialized = useRef(false);
    
      const handleEditorChange = (content, editor) => {
        if (!hasInitialized.current) {
          // Skip the first call
          hasInitialized.current = true;
          return;
        }
        // Only call the user's callback on subsequent changes
        props.onEditorChange?.(content, editor);
      };
    
      return (
        <Editor
          {...props}
          onEditorChange={handleEditorChange}
        />
      );
    }

    I got this code from GitHub copilot – do you think it'd work for you? You'd have to make sure, however, that the initial value (even if you don't pass it using the initialValue prop) is always different to its normalized version, otherwise you'd probably end up with the opposite problem: showing the form not being dirty although the user has already made an edit. As a simple workaround you could just add some trailing or leading spaces to the initial value, either client or server side. Do you think this would work?

  11. Mario-Eis commented on Oct 5, 2025

    @Mario-Eis

    I only tried to stop tinymce from "misfiring". But forcing it to do so and irgnore it every time was not on my radar. Manipulating the content with whitespaces before loading it into the form could theoretically work. That means IF tinymce removes whitespaces on normalized content.

    What we do for the time being:

        if (currentContent !== previousCurrentContentRef.current.current) {
            previousCurrentContentRef.current = {current: currentContent, previous: previousCurrentContentRef.current.current};
        }
    
        useEffect(() => {
            if (normalizedValue !== previousNormalizedValue) {
                // value was changed from outside
                setPreviousNormalizedValue(normalizedValue);
                setInitialValue(normalizedValue);
                setCurrentContent(normalizedValue);
                setInitialContent(null);
                editorRef.current?.resetContent(normalizedValue); // re-trigger the initial normalization
            }
        }, [initialValue, normalizedValue, previousNormalizedValue]);
    
        useEffect(() => {
            // the only possible way to detect if a change event was triggered by an initial automatic normalization is with the addUndo event: See https://github.com/tinymce/tinymce-react/issues/298
            // unfortunately the addUndo event triggers after the editor change event. So we can not fire onChange there. That's why onChange is fired with useEffect.
            if (onChange == null || currentContent === previousCurrentContentRef.current.previous || initialContent == null) {
                return;
            }
    
            const outputValue = currentContent !== initialContent ? currentContent : initialValue;
            onChange(outputValue === "" ? null : outputValue);
            setPreviousNormalizedValue(outputValue);
        }, [onChange, currentContent, initialContent, initialValue]);
    
        const handleEditorChange = useCallback<FieldType<IAllProps, "onEditorChange">>(newValue => {
            // unfortunately the addUndo event is the only place where we can detect initial automatic normalizations. But it triggers after the editor change event. So we can not fire onChange here.
            setCurrentContent(newValue);
        }, []);
    
        const handleAddUndo = useCallback<FieldType<IAllProps, "onAddUndo">>((evt, editor) => {
            if (evt.lastLevel === undefined) {
                // This stores the initial (lastLevel === undefined), normalized content in a state. The state is then used to determine if there were editor changes (editor is dirty).
                // This is needed to prevent issues with markup that was not created directly by tinymce (would look the same, but is different on markup level and would therefore result in a dirty state)
                const newCurrentContent = editor.getContent();
                setInitialContent(newCurrentContent);
                setCurrentContent(newCurrentContent);
                previousCurrentContentRef.current = {current: newCurrentContent, previous: newCurrentContent};
            }
        }, []);

    Basically the idea is to monitor addUndo to get the initial state from there when the undo stack is empty. editorChange is also monitored to get the changed values. Now the problem is, that editorChange fires before addUndo. So we need to track both values in states and compare them in a useEffect to determine wether it was a legitimate user input or an automatic normalization. And another useEffect to reset the whole logic when the props changed (controlled behaviour).

    It works more or less. But unfortunately that is some really serious logic that is hard to follow for new develpers. And we do have no idea for how long that would work and if it works perfect at all. Because the code was developed empirically and uses not offically documented behaviour.

  12. carlosmintfan commented on Oct 5, 2025

    @carlosmintfan

    Glad you already found a workaround! But yes, it‘s quite complicated and probably does take some time to understand. TinyMCE does remove trailing and leading whitespaces, I tested it, so migrating to the workaround I shared could be a simplification although…it‘s hacky in a different way :)
    Anywqy, an important thing to take care of is: If TinyMCE will incorporate the workaround I shared into the codebase of tinymce-react, then this MUST be done in a major version because it is a breaking change for everyone who applied it manually: If both tinymce-react and your code ignore the first event they get, then the first real change event also gets ignored, so then it would be very important to remove the workaround from your codebase before upgrading.

  13. carlosmintfan commented on Oct 7, 2025

    @carlosmintfan

    Note: If a user makes a change and then reverts it, the editor will still be considered dirty if you use my workaround. I'm not sure if your workaround is more stable in this regard.

  14. Mario-Eis commented on Oct 16, 2025

    @Mario-Eis

    Actually i dont really know if my workaround handles the dirty state of tinymce. I would need to have a look. But it is not that important. Because the form manages its own dirty state. When the form resets and tinymce does not fire an onChange imediatly (what the workaround manages to mitigate), the form stays "not dirty". I think by ignoring the first onChange in your workaround, it could also manage to keep the correct state in the form.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions