diff --git a/src/core/classes/async-component.ts b/src/core/classes/async-component.ts
new file mode 100644
index 000000000..77635a70f
--- /dev/null
+++ b/src/core/classes/async-component.ts
@@ -0,0 +1,24 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * Component that is not rendered immediately after being mounted.
+ */
+export interface AsyncComponent {
+
+    /**
+     * Wait until the component is fully rendered and ready.
+     */
+    ready(): Promise<void>;
+}
diff --git a/src/core/components/loading/loading.ts b/src/core/components/loading/loading.ts
index 2fbd213cc..d5be19770 100644
--- a/src/core/components/loading/loading.ts
+++ b/src/core/components/loading/loading.ts
@@ -20,6 +20,7 @@ import { CoreAnimations } from '@components/animations';
 import { Translate } from '@singletons';
 import { CoreComponentsRegistry } from '@singletons/components-registry';
 import { CorePromisedValue } from '@classes/promised-value';
+import { AsyncComponent } from '@classes/async-component';
 
 /**
  * Component to show a loading spinner and message while data is being loaded.
@@ -47,7 +48,7 @@ import { CorePromisedValue } from '@classes/promised-value';
     styleUrls: ['loading.scss'],
     animations: [CoreAnimations.SHOW_HIDE],
 })
-export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
+export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, AsyncComponent {
 
     @Input() hideUntil: unknown; // Determine when should the contents be shown.
     @Input() message?: string; // Message to show while loading.
@@ -58,7 +59,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
     uniqueId: string;
     protected element: HTMLElement; // Current element.
     loaded = false; // Only comes true once.
-    protected firstLoadedPromise = new CorePromisedValue<string>();
+    protected onReadyPromise = new CorePromisedValue<void>();
 
     constructor(element: ElementRef) {
         this.element = element.nativeElement;
@@ -108,7 +109,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
 
         if (!this.loaded && loaded) {
             this.loaded = true; // Only comes true once.
-            this.firstLoadedPromise.resolve(this.uniqueId);
+            this.onReadyPromise.resolve();
         }
 
         // Event has been deprecated since app 4.0.
@@ -119,12 +120,10 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
     }
 
     /**
-     * Wait the loading to finish.
-     *
-     * @return Promise resolved with the uniqueId when done.
+     * @inheritdoc
      */
-    async whenLoaded(): Promise<string> {
-        return await this.firstLoadedPromise;
+    async ready(): Promise<void> {
+        return await this.onReadyPromise;
     }
 
 }
diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts
index f63d1f57c..2dccfd760 100644
--- a/src/core/directives/collapsible-footer.ts
+++ b/src/core/directives/collapsible-footer.ts
@@ -66,7 +66,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
 
         await this.domPromise;
         await this.waitLoadingsDone();
-        await this.waitFormatTextsRendered(this.element);
+        await this.waitFormatTextsRendered();
 
         this.content = this.element.closest('ion-content');
 
@@ -156,15 +156,9 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
 
     /**
      * Wait until all <core-format-text> children inside the element are done rendering.
-     *
-     * @param element Element.
      */
-    protected async waitFormatTextsRendered(element: Element): Promise<void> {
-        await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>(
-            element,
-            'core-format-text',
-            'rendered',
-        );
+    protected async waitFormatTextsRendered(): Promise<void> {
+        await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective);
     }
 
     /**
@@ -210,13 +204,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
         const scrollElement = await this.ionContent.getScrollElement();
 
         await Promise.all([
-            await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>
-            (scrollElement, 'core-loading', 'whenLoaded'),
-            await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(
-                this.element,
-                'core-loading',
-                'whenLoaded',
-            ),
+            await CoreComponentsRegistry.waitComponentsReady(scrollElement, 'core-loading', CoreLoadingComponent),
+            await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-loading', CoreLoadingComponent),
         ]);
     }
 
diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts
index 69f4929ef..0bf4d0738 100644
--- a/src/core/directives/collapsible-header.ts
+++ b/src/core/directives/collapsible-header.ts
@@ -332,15 +332,13 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
 
     /**
      * Wait until all <core-loading> children inside the page.
-     *
-     * @return Promise resolved when loadings are done.
      */
     protected async waitLoadingsDone(): Promise<void> {
-        await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(
-            this.page,
-            'core-loading',
-            'whenLoaded',
-        );
+        if (!this.page) {
+            return;
+        }
+
+        await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent);
     }
 
     /**
@@ -350,11 +348,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
      * @return Promise resolved when texts are rendered.
      */
     protected async waitFormatTextsRendered(element: Element): Promise<void> {
-        await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>(
-            element,
-            'core-format-text',
-            'rendered',
-        );
+        await CoreComponentsRegistry.waitComponentsReady(element, 'core-format-text', CoreFormatTextDirective);
     }
 
     /**
diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts
index 08e987035..41990f722 100644
--- a/src/core/directives/collapsible-item.ts
+++ b/src/core/directives/collapsible-item.ts
@@ -16,7 +16,6 @@ import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
 import { CoreCancellablePromise } from '@classes/cancellable-promise';
 import { CoreLoadingComponent } from '@components/loading/loading';
 import { CoreDomUtils } from '@services/utils/dom';
-import { CoreUtils } from '@services/utils/utils';
 import { Translate } from '@singletons';
 import { CoreComponentsRegistry } from '@singletons/components-registry';
 import { CoreEventObserver } from '@singletons/events';
@@ -103,28 +102,18 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
 
         const page = this.element.closest('.ion-page');
 
-        await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(page, 'core-loading', 'whenLoaded');
+        if (!page) {
+            return;
+        }
+
+        await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent);
     }
 
     /**
      * Wait until all <core-format-text> children inside the element are done rendering.
-     *
-     * @param element Element.
      */
-    protected async waitFormatTextsRendered(element: Element): Promise<void> {
-        let formatTextElements: HTMLElement[] = [];
-
-        if (this.element.tagName == 'CORE-FORMAT-TEXT') {
-            formatTextElements = [this.element];
-        } else {
-            formatTextElements = Array.from(element.querySelectorAll('core-format-text'));
-        }
-
-        const formatTexts = formatTextElements.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective));
-
-        await Promise.all(formatTexts.map(formatText => formatText?.rendered()));
-
-        await CoreUtils.nextTick();
+    protected async waitFormatTextsRendered(): Promise<void> {
+        await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective);
     }
 
     /**
@@ -134,7 +123,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
         // Remove max-height (if any) to calculate the real height.
         this.element.classList.add('collapsible-loading-height');
 
-        await this.waitFormatTextsRendered(this.element);
+        await this.waitFormatTextsRendered();
 
         this.expandedHeight = this.element.getBoundingClientRect().height;
 
diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts
index 42c780f76..afd53beae 100644
--- a/src/core/directives/format-text.ts
+++ b/src/core/directives/format-text.ts
@@ -43,6 +43,7 @@ import { CoreSubscriptions } from '@singletons/subscriptions';
 import { CoreComponentsRegistry } from '@singletons/components-registry';
 import { CoreCollapsibleItemDirective } from './collapsible-item';
 import { CoreCancellablePromise } from '@classes/cancellable-promise';
+import { AsyncComponent } from '@classes/async-component';
 
 /**
  * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
@@ -56,7 +57,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
 @Directive({
     selector: 'core-format-text',
 })
-export class CoreFormatTextDirective implements OnChanges, OnDestroy {
+export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncComponent {
 
     @ViewChild(CoreCollapsibleItemDirective) collapsible?: CoreCollapsibleItemDirective;
 
@@ -137,9 +138,9 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy {
     }
 
     /**
-     * Wait until the text is fully rendered.
+     * @inheritdoc
      */
-    async rendered(): Promise<void> {
+    async ready(): Promise<void> {
         if (!this.element.classList.contains('core-format-text-loading')) {
             return;
         }
diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts
index 550682a86..18a9760fb 100644
--- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts
+++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts
@@ -294,8 +294,11 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
         await this.domPromise;
 
         const page = this.element.closest('.ion-page');
+        if (!page) {
+            return;
+        }
 
-        await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(page, 'core-loading', 'whenLoaded');
+        await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent);
     }
 
     /**
diff --git a/src/core/singletons/components-registry.ts b/src/core/singletons/components-registry.ts
index 5198ee90b..32b08e3dd 100644
--- a/src/core/singletons/components-registry.ts
+++ b/src/core/singletons/components-registry.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import { Component } from '@angular/core';
+import { AsyncComponent } from '@classes/async-component';
 import { CoreUtils } from '@services/utils/utils';
 
 /**
@@ -39,7 +40,7 @@ export class CoreComponentsRegistry {
      * @param componentClass Component class.
      * @returns Component instance.
      */
-    static resolve<T = Component>(element?: Element | null, componentClass?: ComponentConstructor<T>): T | null {
+    static resolve<T>(element?: Element | null, componentClass?: ComponentConstructor<T>): T | null {
         const instance = (element && this.instances.get(element) as T) ?? null;
 
         return instance && (!componentClass || instance instanceof componentClass)
@@ -65,35 +66,45 @@ export class CoreComponentsRegistry {
     }
 
     /**
-     * Waits all elements to be rendered.
+     * Get a component instances and wait to be ready.
      *
-     * @param element Parent element where to search.
-     * @param selector Selector to search on parent.
-     * @param fnName Component function that have to be resolved when rendered.
-     * @param params Params of function that have to be resolved when rendered.
+     * @param element Root element.
+     * @param componentClass Component class.
      * @return Promise resolved when done.
      */
-    static async finishRenderingAllElementsInside<T = Component>(
-        element: Element | undefined | null,
-        selector: string,
-        fnName: string,
-        params?: unknown[],
+    static async waitComponentReady<T extends AsyncComponent>(
+        element: Element | null,
+        componentClass?: ComponentConstructor<T>,
     ): Promise<void> {
-        if (!element) {
+        const instance = this.resolve(element, componentClass);
+        if (!instance) {
             return;
         }
 
-        const components = Array
-            .from(element.querySelectorAll(selector))
-            .map(element => CoreComponentsRegistry.resolve<T>(element));
+        await instance.ready();
+    }
 
-        await Promise.all(components.map(component => {
-            if (!component) {
-                return;
-            }
-
-            return component[fnName].apply(component, params);
-        }));
+    /**
+     * Waits all elements matching to be ready.
+     *
+     * @param element Element where to search.
+     * @param selector Selector to search on parent.
+     * @param componentClass Component class.
+     * @return Promise resolved when done.
+     */
+    static async waitComponentsReady<T extends AsyncComponent>(
+        element: Element,
+        selector: string,
+        componentClass?: ComponentConstructor<T>,
+    ): Promise<void> {
+        if (element.matches(selector)) {
+            // Element to wait is myself.
+            await CoreComponentsRegistry.waitComponentReady<T>(element, componentClass);
+        } else {
+            await Promise.all(Array
+                .from(element.querySelectorAll(selector))
+                .map(element => CoreComponentsRegistry.waitComponentReady<T>(element, componentClass)));
+        }
 
         // Wait for next tick to ensure components are completely rendered.
         await CoreUtils.nextTick();
@@ -105,4 +116,4 @@ export class CoreComponentsRegistry {
  * Component constructor.
  */
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type ComponentConstructor<T> = { new(...args: any[]): T };
+export type ComponentConstructor<T = Component> = { new(...args: any[]): T };