diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow
index 8f2752fe6d0..99004d8b137 100644
--- a/packages/lexical-react/flow/LexicalContentEditable.js.flow
+++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow
@@ -7,16 +7,53 @@
  * @flow strict
  */
 
+import type { LexicalEditor } from 'lexical';
+// $FlowFixMe - Not able to type this with a flow extension
+import type {TRefFor} from 'CoreTypes.flow';
+
 import * as React from 'react';
+import type { AbstractComponent } from "react";
+
+type InlineStyle = {
+  [key: string]: mixed;
+}
+
+// Due to Flow limitations, we prefer fixed types over the built-in inexact HTMLElement
+type HTMLDivElementDOMProps = $ReadOnly<{
+  'aria-label'?: void | string,
+  'aria-labeledby'?: void | string,
+  'title'?: void | string,
+  onClick?: void | (e: SyntheticEvent<HTMLDivElement>) => mixed,
+  autoCapitalize?: void | boolean,
+  autoComplete?: void | boolean,
+  autoCorrect?: void | boolean,
+  id?: void | string,
+  className?: void | string,
+  'data-testid'?: void | string,
+  role?: void | string,
+  spellCheck?: void | boolean,
+  suppressContentEditableWarning?: void | boolean,
+  tabIndex?: void | number,
+  style?: void | InlineStyle | CSSStyleDeclaration,
+  'data-testid'?: void | string,
+}>;
+
+export type PlaceholderProps =
+  | $ReadOnly<{
+      'aria-placeholder'?: void,
+      placeholder?: null,
+    }>
+  | $ReadOnly<{
+      'aria-placeholder': string,
+      placeholder:
+        | ((isEditable: boolean) => null | React$Node)
+        | null
+        | React$Node,
+    }>;
 
-export type Props = ({...Partial<HTMLDivElement>,...} | $ReadOnly<{
-  'aria-placeholder': string;
-  placeholder:
-    | ((isEditable: boolean) => null | React$Node)
-    | null
-    | React$Node;
-}>) & $ReadOnly<{
-  ...Partial<HTMLDivElement>,
+export type Props = $ReadOnly<{
+  ...HTMLDivElementDOMProps,
+  editor__DEPRECATED?: LexicalEditor;
   ariaActiveDescendant?: string,
   ariaAutoComplete?: string,
   ariaControls?: string,
@@ -26,10 +63,13 @@ export type Props = ({...Partial<HTMLDivElement>,...} | $ReadOnly<{
   ariaLabelledBy?: string,
   ariaMultiline?: boolean,
   ariaOwns?: string,
-  ariaRequired?: boolean,
+  ariaRequired?: string,
   autoCapitalize?: boolean,
-  'data-testid'?: string | null,
-  ...
-}>;
+  ref?: TRefFor<HTMLDivElement>,
+  ...PlaceholderProps
+}>
 
-declare export function ContentEditable(props: Props): React$Node;
+declare export var ContentEditable: AbstractComponent<
+  Props,
+  HTMLDivElement,
+>;
diff --git a/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow b/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow
index be50e13f492..7a9e8b403e2 100644
--- a/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow
+++ b/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow
@@ -21,6 +21,9 @@ type InitialEditorStateType =
 
 declare export function PlainTextPlugin({
   contentEditable: React$Node,
-  placeholder: ((isEditable: boolean) => React$Node) | React$Node,
+  placeholder?:
+  | ((isEditable: boolean) => null | React$Node)
+  | null
+  | React$Node;
   ErrorBoundary: LexicalErrorBoundary,
 }): React$Node;
diff --git a/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow b/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow
index a99380c5fe4..a07bf9e92f4 100644
--- a/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow
+++ b/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow
@@ -21,6 +21,9 @@ type InitialEditorStateType =
 
 declare export function RichTextPlugin({
   contentEditable: React$Node,
-  placeholder: ((isEditable: boolean) => React$Node) | React$Node,
+  placeholder?:
+  | ((isEditable: boolean) => null | React$Node)
+  | null
+  | React$Node;
   ErrorBoundary: LexicalErrorBoundary,
 }): React$Node;
diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx
index 2ca5839087e..30829f6fb40 100644
--- a/packages/lexical-react/src/LexicalContentEditable.tsx
+++ b/packages/lexical-react/src/LexicalContentEditable.tsx
@@ -7,50 +7,69 @@
  */
 
 import type {Props as ElementProps} from './shared/LexicalContentEditableElement';
+import type {LexicalEditor} from 'lexical';
 
 import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
-import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
+import {forwardRef, Ref, useLayoutEffect, useState} from 'react';
 
 import {ContentEditableElement} from './shared/LexicalContentEditableElement';
 import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder';
 
 /* eslint-disable @typescript-eslint/ban-types */
-export type Props = (
-  | {}
-  | {
-      'aria-placeholder': string;
-      placeholder:
-        | ((isEditable: boolean) => null | JSX.Element)
-        | null
-        | JSX.Element;
-    }
-) &
-  ElementProps;
+export type Props = Omit<ElementProps, 'editor'> & {
+  editor__DEPRECATED?: LexicalEditor;
+} & (
+    | {
+        'aria-placeholder'?: void;
+        placeholder?: null;
+      }
+    | {
+        'aria-placeholder': string;
+        placeholder:
+          | ((isEditable: boolean) => null | JSX.Element)
+          | JSX.Element;
+      }
+  );
+
 /* eslint-enable @typescript-eslint/ban-types */
 
-export function ContentEditable(props: Props): JSX.Element {
-  let placeholder = null;
-  let rest = props;
-  if ('placeholder' in props) {
-    ({placeholder, ...rest} = props);
-  }
+export const ContentEditable = forwardRef(ContentEditableImpl);
+
+function ContentEditableImpl(
+  props: Props,
+  ref: Ref<HTMLDivElement>,
+): JSX.Element {
+  const {placeholder, editor__DEPRECATED, ...rest} = props;
+  // editor__DEPRECATED will always be defined for non MLC surfaces
+  // eslint-disable-next-line react-hooks/rules-of-hooks
+  const editor = editor__DEPRECATED || useLexicalComposerContext()[0];
 
   return (
     <>
-      <ContentEditableElement {...rest} />
-      <Placeholder content={placeholder} />
+      <ContentEditableElement editor={editor} {...rest} ref={ref} />
+      {placeholder != null && (
+        <Placeholder editor={editor} content={placeholder} />
+      )}
     </>
   );
 }
 
 function Placeholder({
   content,
+  editor,
 }: {
-  content: ((isEditable: boolean) => null | JSX.Element) | null | JSX.Element;
+  editor: LexicalEditor;
+  content: ((isEditable: boolean) => null | JSX.Element) | JSX.Element;
 }): null | JSX.Element {
-  const [editor] = useLexicalComposerContext();
   const showPlaceholder = useCanShowPlaceholder(editor);
-  const editable = useLexicalEditable();
+
+  const [isEditable, setEditable] = useState(editor.isEditable());
+  useLayoutEffect(() => {
+    setEditable(editor.isEditable());
+    return editor.registerEditableListener((currentIsEditable) => {
+      setEditable(currentIsEditable);
+    });
+  }, [editor]);
 
   if (!showPlaceholder) {
     return null;
@@ -58,7 +77,7 @@ function Placeholder({
 
   let placeholder = null;
   if (typeof content === 'function') {
-    placeholder = content(editable);
+    placeholder = content(isEditable);
   } else if (content !== null) {
     placeholder = content;
   }
diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx
index 328da5d4028..2e1208e0d64 100644
--- a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx
+++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx
@@ -6,12 +6,16 @@
  *
  */
 
-import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
+import type {LexicalEditor} from 'lexical';
+
 import * as React from 'react';
-import {useCallback, useState} from 'react';
+import {forwardRef, Ref, useCallback, useMemo, useState} from 'react';
 import useLayoutEffect from 'shared/useLayoutEffect';
 
+import {mergeRefs} from './mergeRefs';
+
 export type Props = {
+  editor: LexicalEditor;
   ariaActiveDescendant?: React.AriaAttributes['aria-activedescendant'];
   ariaAutoComplete?: React.AriaAttributes['aria-autocomplete'];
   ariaControls?: React.AriaAttributes['aria-controls'];
@@ -26,31 +30,34 @@ export type Props = {
   'data-testid'?: string | null | undefined;
 } & Omit<React.AllHTMLAttributes<HTMLDivElement>, 'placeholder'>;
 
-export function ContentEditableElement({
-  ariaActiveDescendant,
-  ariaAutoComplete,
-  ariaControls,
-  ariaDescribedBy,
-  ariaExpanded,
-  ariaLabel,
-  ariaLabelledBy,
-  ariaMultiline,
-  ariaOwns,
-  ariaRequired,
-  autoCapitalize,
-  className,
-  id,
-  role = 'textbox',
-  spellCheck = true,
-  style,
-  tabIndex,
-  'data-testid': testid,
-  ...rest
-}: Props): JSX.Element {
-  const [editor] = useLexicalComposerContext();
-  const [isEditable, setEditable] = useState(false);
+function ContentEditableElementImpl(
+  {
+    editor,
+    ariaActiveDescendant,
+    ariaAutoComplete,
+    ariaControls,
+    ariaDescribedBy,
+    ariaExpanded,
+    ariaLabel,
+    ariaLabelledBy,
+    ariaMultiline,
+    ariaOwns,
+    ariaRequired,
+    autoCapitalize,
+    className,
+    id,
+    role = 'textbox',
+    spellCheck = true,
+    style,
+    tabIndex,
+    'data-testid': testid,
+    ...rest
+  }: Props,
+  ref: Ref<HTMLDivElement>,
+): JSX.Element {
+  const [isEditable, setEditable] = useState(editor.isEditable());
 
-  const ref = useCallback(
+  const handleRef = useCallback(
     (rootElement: null | HTMLElement) => {
       // defaultView is required for a root element.
       // In multi-window setups, the defaultView may not exist at certain points.
@@ -66,6 +73,7 @@ export function ContentEditableElement({
     },
     [editor],
   );
+  const mergedRefs = useMemo(() => mergeRefs(ref, handleRef), [handleRef, ref]);
 
   useLayoutEffect(() => {
     setEditable(editor.isEditable());
@@ -77,33 +85,31 @@ export function ContentEditableElement({
   return (
     <div
       {...rest}
-      aria-activedescendant={!isEditable ? undefined : ariaActiveDescendant}
-      aria-autocomplete={!isEditable ? 'none' : ariaAutoComplete}
-      aria-controls={!isEditable ? undefined : ariaControls}
+      aria-activedescendant={isEditable ? ariaActiveDescendant : undefined}
+      aria-autocomplete={isEditable ? ariaAutoComplete : 'none'}
+      aria-controls={isEditable ? ariaControls : undefined}
       aria-describedby={ariaDescribedBy}
       aria-expanded={
-        !isEditable
-          ? undefined
-          : role === 'combobox'
-          ? !!ariaExpanded
-          : undefined
+        isEditable && role === 'combobox' ? !!ariaExpanded : undefined
       }
       aria-label={ariaLabel}
       aria-labelledby={ariaLabelledBy}
       aria-multiline={ariaMultiline}
-      aria-owns={!isEditable ? undefined : ariaOwns}
-      aria-readonly={!isEditable ? true : undefined}
+      aria-owns={isEditable ? ariaOwns : undefined}
+      aria-readonly={isEditable ? undefined : true}
       aria-required={ariaRequired}
       autoCapitalize={autoCapitalize}
       className={className}
       contentEditable={isEditable}
       data-testid={testid}
       id={id}
-      ref={ref}
-      role={role}
+      ref={mergedRefs}
+      role={isEditable ? role : undefined}
       spellCheck={spellCheck}
       style={style}
       tabIndex={tabIndex}
     />
   );
 }
+
+export const ContentEditableElement = forwardRef(ContentEditableElementImpl);
diff --git a/packages/lexical-react/src/shared/mergeRefs.ts b/packages/lexical-react/src/shared/mergeRefs.ts
new file mode 100644
index 00000000000..23ddadd0620
--- /dev/null
+++ b/packages/lexical-react/src/shared/mergeRefs.ts
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+// Source: https://github.com/gregberge/react-merge-refs/blob/main/src/index.tsx
+
+export function mergeRefs<T>(
+  ...refs: Array<
+    React.MutableRefObject<T> | React.LegacyRef<T> | undefined | null
+  >
+): React.RefCallback<T> {
+  return (value) => {
+    refs.forEach((ref) => {
+      if (typeof ref === 'function') {
+        ref(value);
+      } else if (ref != null) {
+        (ref as React.MutableRefObject<T | null>).current = value;
+      }
+    });
+  };
+}