State Management Patterns for Editor Components in React-Based LMS Platforms

state react pattern

If youโ€™re building a Learning Management System (LMS) in React, youโ€™ve probably faced this annoying issue: you add a WYSIWYG editor, and suddenly it turns slow. You type, and suddenly the cursor jumps around or the editor re-renders for no reason.

And honestly, the issue isnโ€™t the editor itself; itโ€™s usually the way we handle its state.

WYSIWYG editors like Froala, TinyMCE, and Draft.js are great for creating content, but they come with their own internal state. When we try to sync that state with React (using Redux, Context, or even normal component state), things can get messy really fast.

This post covers simple, beginner-friendly patterns to integrate WYSIWYG editors into a React-based LMS without performance issues.

Key Takeaways

  • Initialise the editor only when needed so it doesnโ€™t re-render too much.
  • Use refs instead of controlled components to keep typing smooth.
  • Debounce state updates to prevent performance degradation during typing.
  • Load the editor after a user action (like clicking a button) instead of loading it immediately.
  • Keep the editorโ€™s content separate from other fast-changing app state to avoid slowdowns.

Understanding the Problem

Before we jump into solutions, letโ€™s first understandย whyย WYSIWYG editors can be tricky in React.

React works best withย controlled components, where it manages the state and updates it on every change. But WYSIWYG editors donโ€™t really work that way. They manage a lot of internal stuff on their own: cursor position, text selection, formatting, undo history, and more.

So when we try to make them fully controlled in React, every single keystroke triggers a React state update. That update causes a re-render, which then interferes with the editorโ€™s internal state.

And the result? Laggy typing, the cursor jumping around, and a very frustrated user experience.

Now that we know what causes the lag, letโ€™s look at how to set up a WYSIWYG editor properly before applying the patterns.

Getting Started with a WYSIWYG Editor in React

Before implementing the patterns below, youโ€™ll need to install and set up the editor in your React project.

Installation

First, install the editor package in your React project:

 

npm install react-froala-wysiwyg --save

 

Basic Setup

Now add the following code toย src/components/SimpleEditor.jsxย to set up a simple editor in your React project:

 

// Import Editor styles and scripts
import "froala-editor/css/froala_style.min.css";
import "froala-editor/css/froala_editor.pkgd.min.css";
import FroalaEditorComponent from "react-froala-wysiwyg";

const SimpleEditor = () => {
  return (
    <FroalaEditorComponent
      tag="textarea"
      config={{
        placeholderText: "Start typing...",
        toolbarButtons: ["bold", "italic", "underline", "undo", "redo"],
        height: 300,
        width: 800,
        events: {
          contentChanged: function () {
            console.log("Content updated!");
          },
          initialized: function () {
            console.log("Editor is ready!");
          },
        },
      }}
    />
  );
};

export default SimpleEditor;

 

 

What these options mean:

  • tag: The HTML element the editor is built on (textareaย orย div).
  • placeholderText: What shows when the editor is empty.
  • toolbarButtons: Choose which toolbar buttons to show.
  • height: Sets the editorโ€™s height.
  • width: Sets the editorโ€™s width.
  • events: Lets you listen to things likeย contentChangedย orย initializedย so you can react to editor actions.

Now, import and use it in yourย App.jsxย file:

 

importย SimpleEditorย fromย './components/SimpleEditor';

functionย App() {
returnย <SimpleEditorย />;
}

 

 

Hereโ€™s what it will look like:

froala editor

Now that you have the editor set up, letโ€™s look at different patterns to manage its state without hurting performance.

Pattern 1: Using Refs with Uncontrolled Editors (Simplest Approach)

The easiest way to work with a WYSIWYG editor in React is to treat it as an uncontrolled component.

Instead of syncing every keystroke with React, you just use a ref to access the content whenever you need it.

Create a new fileย src/components/LessonEditor.jsxย and add the following code inside it:

 

import React, { useRef, useState } from "react";
import FroalaEditorComponent from "react-froala-wysiwyg";

function LessonEditor() {
  const editorRef = useRef(null);
  const [isSaving, setIsSaving] = useState(false);

  const handleSave = async () => {
    // Get content only when needed
    const content = editorRef.current?.editor?.html?.get();
    setIsSaving(true);
    try {
      await saveToDatabase(content);
      alert("Lesson saved successfully!");
    } catch (error) {
      alert("Error saving lesson");
    }
    setIsSaving(false);
  };

  return (
    <div>
      <h2>Create Your Lesson</h2>
      <FroalaEditorComponent
        ref={editorRef}
        tag="textarea"
        config={{
          placeholderText: "Start writing your lesson content...",
          height: 400,
          width: 900,
        }}
      />
      <button
        onClick={handleSave}
        disabled={isSaving}
        style={{
          marginTop: "10px",
          padding: "10px 20px",
          fontSize: "16px",
          cursor: "pointer",
          backgroundColor: "#3c55c4ff",
          color: "white",
          border: "none",
          borderRadius: "5px",
        }}
      >
        {isSaving ? "Saving..." : "Save Lesson"}
      </button>
    </div>
  );
}

export default LessonEditor;

 

 

What this code does:

  • useRefย creates a reference to the editor so you can read its content whenever you want.
  • Reactย does notย try to manage the editorโ€™s internal state; the editor handles everything itself.
  • When the user clicksย Save, we grab the content using the ref and send it to the database.
  • While saving, the button showsย Savingโ€ฆย so the user knows something is happening.
  • This approach keeps typing smooth and removes the lag caused by constant re-renders.

Now import it wherever you want to show the editor:

 

import LessonEditor from './components/LessonEditor';

function App() {
  return <LessonEditor />;
}

 

 

froala editor create lesson demo

Why this works:ย The editor keeps its own internal state, including cursor position, formatting, and undo history. Since React isnโ€™t watching every keystroke, nothing slows down. You only read the content when needed (like saving), which keeps your LMS fast and smooth.

This pattern works perfectly when you have a single editor. But what if your LMS page has multiple editors, like quizzes or modules? Thatโ€™s where Pattern 2 comes in.

Pattern 2: Lazy Initialisation with onClick

When youโ€™re building LMS features, you might have multiple editors on the same page, like quiz questions, explanations, hints, etc.

Loading all editors at once can slow everything down.

A simple fix is to initialise the editor only when the user clicks on it.

This keeps your page fast and avoids unnecessary memory usage.

Create a new fileย src/components/QuizQuestionEditor.jsxย and add the following code inside it:

 

import React, { useState, useRef } from "react";
import FroalaEditor from "react-froala-wysiwyg";

function QuizQuestionEditor() {
  const [editorActive, setEditorActive] = useState(false);
  const editorRef = useRef(null);
  const [initialContent] = useState("");

  const activateEditor = () => {
    setEditorActive(true);
  };

  const handleSubmit = () => {
    if (editorRef.current) {
      const content = editorRef.current.editor.html.get();
      console.log("Question content:", content);
      // Submit to your backend
    }
  };

  return (
    <div>
      <h2>Quiz Question</h2>
      {!editorActive ? (
        <div
          onClick={activateEditor}
          style={{
            border: "1px solid #ccc",
            padding: "20px",
            borderRadius: "4px",
            cursor: "pointer",
            backgroundColor: "#f9f9f9",
          }}
        >
          Click to start writing your question...
        </div>
      ) : (
        <FroalaEditor
          ref={editorRef}
          tag="textarea"
          model={initialContent}
          config={{
            placeholderText: "Enter your quiz question here...",
            height: 400,
            width: 900,
          }}
        />
      )}
      <button
        onClick={handleSubmit}
        style={{
          marginTop: "10px",
          padding: "10px 20px",
          fontSize: "16px",
          cursor: "pointer",
          backgroundColor: "#3c55c4ff",
          color: "white",
          border: "none",
          borderRadius: "5px",
        }}
      >
        Submit Question
      </button>
    </div>
  );
}

export default QuizQuestionEditor;

 

What this code does:

  • The editor doesnโ€™t load immediately. Instead, you show a placeholder box:ย โ€œClick to start writing your questionโ€ฆโ€
  • When the user clicks,ย editorActiveย becomesย true, and the editor finally loads.
  • The editorโ€™s content is accessed using a ref whenever you submit the question.
  • This avoids loading multiple heavy editors at once and is great for quiz creation pages.

Now import it wherever you want to show the editor:

 

importย QuizQuestionEditorย fromย './components/QuizQuestionEditor';

functionย App() {
returnย <QuizQuestionEditorย />;
}

 

Lazy loading of a Froala editor when the user clicks an input area

Benefits of this approach:

  • Faster initial page load.
  • Reduced memory usage (because editors load only when needed).
  • Better user experience when working with forms that contain multiple editors.

Lazy loading helps when you have many editors, but what if you also need autosave? Thatโ€™s where Pattern 3 comes in.

Pattern 3: Debounced Updates with Context API

Sometimes youย doย need the editorโ€™s content inside your app state, like when you want autosave.

But updating the state onย every keystrokeย will slow things down immediately.

A simple fix is to useย debouncing: wait a short time after the user stops typing before updating the state.

Create a new fileย src/components/AutoSaveEditor.jsxย and add the following code inside it:

 

import React, { useState, useRef } from "react";
import FroalaEditor from "react-froala-wysiwyg";

function QuizQuestionEditor() {
  const [editorActive, setEditorActive] = useState(false);
  const editorRef = useRef(null);
  const [initialContent] = useState("");

  const activateEditor = () => {
    setEditorActive(true);
  };

  const handleSubmit = () => {
    if (editorRef.current) {
      const content = editorRef.current.editor.html.get();
      console.log("Question content:", content);
      // Submit to your backend
    }
  };

  return (
    <div>
      <h2>Quiz Question</h2>
      {!editorActive ? (
        <div
          onClick={activateEditor}
          style={{
            border: "1px solid #ccc",
            padding: "20px",
            borderRadius: "4px",
            cursor: "pointer",
            backgroundColor: "#f9f9f9",
          }}
        >
          Click to start writing your question...
        </div>
      ) : (
        <FroalaEditor
          ref={editorRef}
          tag="textarea"
          model={initialContent}
          config={{
            placeholderText: "Enter your quiz question here...",
            height: 400,
            width: 900,
          }}
        />
      )}
      <button
        onClick={handleSubmit}
        style={{
          marginTop: "10px",
          padding: "10px 20px",
          fontSize: "16px",
          cursor: "pointer",
          backgroundColor: "#3c55c4ff",
          color: "white",
          border: "none",
          borderRadius: "5px",
        }}
      >
        Submit Question
      </button>
    </div>
  );
}

export default QuizQuestionEditor;

 

 

 

What this code does:

  • The editor fires aย contentChangedย event every time the user types.
  • But instead of updating the state instantly, we debounce it usingย setTimeout.
  • If the user keeps typing, the timer resets.
  • When they stop typing for 2 seconds, then:
  • The content is saved into global state (LessonContext)
  • The autosave request is sent to the backend
  • A small message shows when the lesson was last saved.

Now, wrap your app in theย LessonProvider:

 

import { LessonProvider, AutoSaveEditor } from './components/AutoSaveEditor';

function App() {
  return (
    <LessonProvider>
      <AutoSaveEditor />
    </LessonProvider>
  );
}

 

Froala editor with an autosave message showing the last saved time

Key points:

  • We debounce editor changes to avoid constant state updates.
  • Autosave feels fast but never slows down typing.
  • Using Context lets other components read the lesson content too.
  • Perfect for LMS dashboards with autosave, drafts, and live editing.

No matter which pattern you use, a few best practices can save you from unexpected performance issues.

Best Practices

Here are a few simple guidelines to keep your WYSIWYG editors fast, clean, and easy to manage in React, especially when youโ€™re building LMS features.

  1. Initialise on demand: Donโ€™t load the editor until the user actually needs it. This really helps when you have multiple editors on a single page.
  2. Use refs for content access: Instead of making the editor fully controlled, use refs to access the content only when you need it.
  3. Add debouncing for state updates:ย If youโ€™re syncing editor content with state (like autosave), debounce it by 1โ€“2 seconds so youโ€™re not updating constantly.
  4. Keep editor state separate: Avoid mixing editor HTML with other UI state. Keep it separate from things like loading flags, filters, or user preferences.
  5. Clean up when the component unmounts: Always clear timers, intervals, or event listeners to avoid memory leaks.

And of course, there are a few easy mistakes that can undo all this good work. So now look at what to avoid.

Common Pitfalls

These are the mistakes that usually cause WYSIWYG editors to lag, re-render too much, or behave unpredictably in React. Avoiding them will make your editor setup much smoother.

  1. Making the editor fully controlled: Avoid using aย valueย prop that updates on every change. This forces React to re-render constantly, causing cursor jumps and laggy typing.
  2. Storing HTML in a frequently updated state: Donโ€™t mix editor content with state that updates often (like loading flags, validation, or UI filters). It leads to unnecessary re-renders.
  3. Not debouncing autosave: Saving to the backend on every character typed will crush your server and slow down the editor.
  4. Initialising too early: Loading all editors as soon as the page mounts wastes memory. Use lazy initialisation so the editor loads only when needed.
  5. Forgetting to clean up: Forgetting to clear timeouts, intervals, or listeners can cause memory leaks, especially in single-page applications like React apps.

Conclusion

Managing WYSIWYG editors in React doesnโ€™t have to be difficult. The main thing to remember is that these editors already handle a lot of their own internal state. When we try to control every update through React, things slow down, and typing becomes laggy.

By keeping the editor uncontrolled, using refs to read content, debouncing updates, and loading the editor only when needed, you can build fast, smooth LMS features without the usual headaches.

Start with the simplest pattern that fits your use case. You can always add more advanced logic later as your app grows. Just keep in mind: you donโ€™t need to over-optimise too early, but planning for performance from the beginning simply helps you avoid issues later.

Resources for Further Reading

Posted on January 23, 2026

Shefali

Shefali Jangid is a web developer, technical writer, and content creator with a love for building intuitive tools and resources for developers.

She writes about web development, shares practical coding tips on her blog shefali.dev, and creates projects that make developersโ€™ lives easier.

No comment yet, add your voice below!


Add a Comment

Your email address will not be published. Required fields are marked *