Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .changeset/remove-change-observable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
'@portabletext/editor': major
---

feat!: remove `change$` observable and `rxjs` dependency

The `PortableTextEditor.change$` observable isn't used internally anymore. It's
purely a legacy public API that allows consumers to read editor changes/events
through an Observable rather than through the ordinary subscription API. This
does not justify declaring `rxjs` as a peer dependency of PTE and therefore
this commit removes the `change$` Observable along with the `rxjs` dependency.

BREAKING CHANGES:
- Removed `PortableTextEditor.change$` property
- Removed `EditorChanges` type export (`Subject<EditorChange>`)
- Removed `PatchObservable` type export (`Observable<...>`)
- Removed `rxjs` from peerDependencies

Migration: Replace `change$.subscribe()` with `editor.on()` or
`<EventListenerPlugin on={...} />`. Every active `EditorChange` type
has a 1:1 equivalent in the new event API.

What's preserved:
- `EditorChange` type (still used by sanity's `onEditorChange` prop)
- `PortableTextEditor` class (separate deprecation path)
- All static methods and `schemaTypes` property

### Migration

Replace `change$.subscribe()` with `editor.on()` or `<EventListenerPlugin>`:

**Before (rxjs):**

```tsx
import {usePortableTextEditor} from '@portabletext/editor'

const editor = usePortableTextEditor()

useEffect(() => {
const sub = editor.change$.subscribe((change) => {
if (change.type === 'mutation') {
// ...
}
})
return () => sub.unsubscribe()
}, [editor])
```

**After (editor.on):**

```tsx
import {useEditor} from '@portabletext/editor'

const editor = useEditor()

useEffect(() => {
const unsubscribe = editor.on('mutation', (event) => {
// ...
})
return unsubscribe
}, [editor])
```

**After (EventListenerPlugin):**

```tsx
import {EventListenerPlugin} from '@portabletext/editor/plugins'

<EventListenerPlugin
on={(event) => {
if (event.type === 'mutation') {
// ...
}
}}
/>
```

Every active `EditorChange` type has a 1:1 equivalent in the new event API. See the [event listener documentation](https://www.portabletext.org/reference/event-listener-plugin/) for details.
4 changes: 1 addition & 3 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@
"racejar": "workspace:*",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"rxjs": "^7.8.2",
"typescript": "catalog:",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1",
Expand All @@ -128,8 +127,7 @@
},
"peerDependencies": {
"@portabletext/sanity-bridge": "workspace:^2.0.2",
"react": "^19.2.3",
"rxjs": "^7.8.2"
"react": "^19.2.3"
},
"engines": {
"node": ">=20.19 <22 || >=22.12"
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/editor/Editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export const PortableTextEditable = forwardRef<
if (slateRange) {
Transforms.select(slateEditor, slateRange)
// Output selection here in those cases where the editor selection was the same, and there are no set_selection operations made.
// The selection is usually automatically emitted to change$ by the withPortableTextSelections plugin whenever there is a set_selection operation applied.
// The selection is usually automatically emitted by the withPortableTextSelections plugin whenever there is a set_selection operation applied.
if (!slateEditor.operations.some((o) => o.type === 'set_selection')) {
editorActor.send({
type: 'update selection',
Expand Down
6 changes: 0 additions & 6 deletions packages/editor/src/editor/PortableTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import type {
PortableTextObject,
Schema,
} from '@portabletext/schema'
import {Subject} from 'rxjs'
import type {
AddedAnnotationPaths,
EditableAPI,
EditableAPIDeleteOptions,
EditorChanges,
EditorSelection,
} from '../types/editor'
import type {Path} from '../types/paths'
Expand All @@ -33,10 +31,6 @@ import type {InternalEditor} from './create-editor'
* ```
*/
export class PortableTextEditor {
/**
* An observable of all the editor changes.
*/
public change$: EditorChanges = new Subject()
/**
* A lookup table for all the relevant schema types for this portable text type.
*/
Expand Down
15 changes: 1 addition & 14 deletions packages/editor/src/editor/editor-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {Slate} from '../slate-react'
import {createInternalEditor} from './create-editor'
import {EditorActorContext} from './editor-actor-context'
import {EditorContext} from './editor-context'
import {eventToChange} from './event-to-change'
import {PortableTextEditor} from './PortableTextEditor'
import {RelayActorContext} from './relay-actor-context'
import {PortableTextEditorContext} from './usePortableTextEditor'
Expand Down Expand Up @@ -54,18 +53,6 @@ export function EditorProvider(props: EditorProviderProps) {
unsubscribers.push(subscription())
}

const relayActorSubscription = internalEditor.actors.relayActor.on(
'*',
(event) => {
const change = eventToChange(event)

if (change) {
portableTextEditor.change$.next(change)
}
},
)
unsubscribers.push(relayActorSubscription.unsubscribe)

internalEditor.actors.editorActor.start()
internalEditor.actors.editorActor.send({
type: 'add slate editor',
Expand All @@ -85,7 +72,7 @@ export function EditorProvider(props: EditorProviderProps) {
stopActor(internalEditor.actors.relayActor)
stopActor(internalEditor.actors.syncActor)
}
}, [internalEditor, portableTextEditor])
}, [internalEditor])

return (
<EditorContext.Provider value={internalEditor.editor}>
Expand Down
2 changes: 0 additions & 2 deletions packages/editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export type {
EditableAPI,
EditableAPIDeleteOptions,
EditorChange,
EditorChanges,
EditorSelection,
EditorSelectionPoint,
ErrorChange,
Expand All @@ -73,7 +72,6 @@ export type {
OnPasteResultOrPromise,
PasteData,
PatchChange,
PatchObservable,
RangeDecoration,
RangeDecorationOnMovedDetails,
ReadyChange,
Expand Down
19 changes: 0 additions & 19 deletions packages/editor/src/plugins/plugin.internal.change-ref.tsx

This file was deleted.

27 changes: 27 additions & 0 deletions packages/editor/src/plugins/plugin.internal.editor-change-ref.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {useEffect} from 'react'
import {eventToChange} from '../editor/event-to-change'
import {useEditor} from '../editor/use-editor'
import type {EditorChange} from '../types/editor'

export function InternalEditorChangePlugin(props: {
onChange: (change: EditorChange) => void
}) {
const editor = useEditor()
const {onChange} = props

useEffect(() => {
const subscription = editor.on('*', (event) => {
const change = eventToChange(event)

if (change) {
onChange(change)
}
})

return () => {
subscription.unsubscribe()
}
}, [editor, onChange])

return null
}
12 changes: 0 additions & 12 deletions packages/editor/src/types/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import type {
ReactElement,
RefObject,
} from 'react'
import type {Observable, Subject} from 'rxjs'
import type {PortableTextEditableProps} from '../editor/Editable'
import type {EditorSchema} from '../editor/editor-schema'
import type {PortableTextEditor} from '../editor/PortableTextEditor'
Expand Down Expand Up @@ -288,11 +287,6 @@ export type EditorChange =
| UnsetChange
| ValueChange

/**
* @beta
*/
export type EditorChanges = Subject<EditorChange>

/** @beta */
export type OnPasteResult =
| {
Expand Down Expand Up @@ -330,12 +324,6 @@ export type OnCopyFn = (
event: ClipboardEvent<HTMLDivElement | HTMLSpanElement>,
) => undefined | unknown

/** @beta */
export type PatchObservable = Observable<{
patches: Patch[]
snapshot: PortableTextBlock[] | undefined
}>

/** @beta */
export interface BlockRenderProps {
children: ReactElement<any>
Expand Down
16 changes: 8 additions & 8 deletions packages/editor/tests/PortableTextEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {createRef, type RefObject} from 'react'
import {describe, expect, it, vi} from 'vitest'
import type {EditorSelection} from '../src'
import {PortableTextEditor} from '../src/editor/PortableTextEditor'
import {InternalChange$Plugin} from '../src/plugins/plugin.internal.change-ref'
import {InternalEditorChangePlugin} from '../src/plugins/plugin.internal.editor-change-ref'
import {InternalPortableTextEditorRefPlugin} from '../src/plugins/plugin.internal.portable-text-editor-ref'
import {createTestEditor} from '../src/test/vitest'

Expand All @@ -24,7 +24,7 @@ describe('initialization', () => {
const {locator} = await createTestEditor({
children: (
<>
<InternalChange$Plugin onChange={onChange} />
<InternalEditorChangePlugin onChange={onChange} />
<InternalPortableTextEditorRefPlugin ref={editorRef} />
</>
),
Expand All @@ -46,7 +46,7 @@ describe('initialization', () => {
await createTestEditor({
children: (
<>
<InternalChange$Plugin onChange={onChange} />
<InternalEditorChangePlugin onChange={onChange} />
<InternalPortableTextEditorRefPlugin ref={editorRef} />
</>
),
Expand Down Expand Up @@ -80,7 +80,7 @@ describe('initialization', () => {
await createTestEditor({
children: (
<>
<InternalChange$Plugin onChange={onChange} />
<InternalEditorChangePlugin onChange={onChange} />
<InternalPortableTextEditorRefPlugin ref={editorRef} />
</>
),
Expand Down Expand Up @@ -121,7 +121,7 @@ describe('initialization', () => {
await createTestEditor({
children: (
<>
<InternalChange$Plugin onChange={onChange} />
<InternalEditorChangePlugin onChange={onChange} />
<InternalPortableTextEditorRefPlugin ref={editorRef} />
</>
),
Expand Down Expand Up @@ -166,7 +166,7 @@ describe('initialization', () => {
await createTestEditor({
children: (
<>
<InternalChange$Plugin onChange={onChange} />
<InternalEditorChangePlugin onChange={onChange} />
<InternalPortableTextEditorRefPlugin ref={editorRef} />
</>
),
Expand Down Expand Up @@ -211,7 +211,7 @@ describe('initialization', () => {
const {editor} = await createTestEditor({
children: (
<>
<InternalChange$Plugin onChange={onChange} />
<InternalEditorChangePlugin onChange={onChange} />
</>
),
initialValue: value,
Expand Down Expand Up @@ -289,7 +289,7 @@ describe('initialization', () => {
await createTestEditor({
children: (
<>
<InternalChange$Plugin onChange={onChange} />
<InternalEditorChangePlugin onChange={onChange} />
<InternalPortableTextEditorRefPlugin ref={editorRef} />
</>
),
Expand Down
44 changes: 0 additions & 44 deletions packages/editor/tests/change-subject.test.tsx

This file was deleted.

Loading