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
5 changes: 5 additions & 0 deletions .changeset/warm-months-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'slate-react': patch
---

Fix rendering issues when both chunking and React's strict mode are enabled
2 changes: 0 additions & 2 deletions packages/slate-react/src/chunking/reconcile-children.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ export const reconcileChildren = (
debug,
}: ReconcileOptions
) => {
chunkTree.modifiedChunks.clear()

const chunkTreeHelper = new ChunkTreeHelper(chunkTree, { chunkSize, debug })
const childrenHelper = new ChildrenHelper(editor, children)

Expand Down
14 changes: 12 additions & 2 deletions packages/slate-react/src/components/chunk-tree.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Fragment } from 'react'
import React, { ComponentProps, Fragment, useEffect } from 'react'
import { Element } from 'slate'
import { Key } from 'slate-dom'
import { RenderChunkProps } from './editable'
Expand Down Expand Up @@ -51,7 +51,17 @@ const ChunkAncestor = <C extends TChunkAncestor>(props: {
})
}

const ChunkTree = ChunkAncestor<TChunkTree>
const ChunkTree = (props: ComponentProps<typeof ChunkAncestor<TChunkTree>>) => {
// Clear the set of modified chunks only when React finishes rendering. The
// timing of this is important in strict mode because if the chunks are
// cleared during rendering (such as in reconcileChildren), strict mode's
// second render won't include them.
useEffect(() => {
props.root.modifiedChunks.clear()
})

return <ChunkAncestor {...props} />
}

const MemoizedChunk = React.memo(
ChunkAncestor<TChunk>,
Expand Down
51 changes: 37 additions & 14 deletions site/examples/js/huge-document.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { faker } from '@faker-js/faker'
import React, { useCallback, useEffect, useState } from 'react'
import React, { StrictMode, useCallback, useEffect, useState } from 'react'
import { createEditor as slateCreateEditor, Editor } from 'slate'
import { Editable, Slate, withReact, useSelected } from 'slate-react'

Expand Down Expand Up @@ -41,6 +41,7 @@ const initialConfig = {
'chunk'
),
showSelectedHeadings: parseBoolean('selected_headings', false),
strictMode: parseBoolean('strict', false),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a checkbox to the huge document example to enable strict mode for testing. Due to the nature of strict mode, this only works when running the examples locally.

image

}
const setSearchParams = config => {
if (searchParams) {
Expand All @@ -54,6 +55,7 @@ const setSearchParams = config => {
'selected_headings',
config.showSelectedHeadings ? 'true' : 'false'
)
searchParams.set('strict', config.strictMode ? 'true' : 'false')
history.replaceState({}, '', `?${searchParams.toString()}`)
}
}
Expand Down Expand Up @@ -129,6 +131,24 @@ const HugeDocumentExample = () => {
),
[config.contentVisibilityMode, config.chunkOutlines]
)
const editable = rendering ? (
<div>Rendering&hellip;</div>
) : (
<Slate key={editorVersion} editor={editor} initialValue={initialValue}>
<Editable
placeholder="Enter some text…"
renderElement={renderElement}
renderChunk={config.chunkDivs ? renderChunk : undefined}
spellCheck
autoFocus
/>
</Slate>
)
const editableWithStrictMode = config.strictMode ? (
<StrictMode>{editable}</StrictMode>
) : (
editable
)
return (
<>
<PerformanceControls
Expand All @@ -137,19 +157,7 @@ const HugeDocumentExample = () => {
setConfig={setConfig}
/>

{rendering ? (
<div>Rendering&hellip;</div>
) : (
<Slate key={editorVersion} editor={editor} initialValue={initialValue}>
<Editable
placeholder="Enter some text…"
renderElement={renderElement}
renderChunk={config.chunkDivs ? renderChunk : undefined}
spellCheck
autoFocus
/>
</Slate>
)}
{editableWithStrictMode}
</>
)
}
Expand Down Expand Up @@ -394,6 +402,21 @@ const PerformanceControls = ({ editor, config, setConfig }) => {
Call <code>useSelected</code> in each heading
</label>
</p>

<p>
<label>
<input
type="checkbox"
checked={config.strictMode}
onChange={event =>
setConfig({
strictMode: event.target.checked,
})
}
/>{' '}
React strict mode (only works in localhost)
</label>
</p>
</details>

<details>
Expand Down
13 changes: 5 additions & 8 deletions site/examples/js/richtext.jsx
Copy link
Contributor Author

@12joan 12joan Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file was changed as a result of yarn tsc:examples

Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,9 @@ const BlockButton = ({ format, icon }) => {
format,
isAlignType(format) ? 'align' : 'type'
)}
onMouseDown={event => {
event.preventDefault()
toggleBlock(editor, format)
}}
onPointerDown={event => event.preventDefault()}
onClick={() => toggleBlock(editor, format)}
data-test-id={`block-button-${format}`}
>
<Icon>{icon}</Icon>
</Button>
Expand All @@ -208,10 +207,8 @@ const MarkButton = ({ format, icon }) => {
return (
<Button
active={isMarkActive(editor, format)}
onMouseDown={event => {
event.preventDefault()
toggleMark(editor, format)
}}
onPointerDown={event => event.preventDefault()}
onClick={() => toggleMark(editor, format)}
>
<Icon>{icon}</Icon>
</Button>
Expand Down
53 changes: 40 additions & 13 deletions site/examples/ts/huge-document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker'
import React, {
CSSProperties,
Dispatch,
StrictMode,
useCallback,
useEffect,
useState,
Expand Down Expand Up @@ -33,6 +34,7 @@ interface Config {
chunkOutlines: boolean
contentVisibilityMode: 'none' | 'element' | 'chunk'
showSelectedHeadings: boolean
strictMode: boolean
}

const blocksOptions = [
Expand Down Expand Up @@ -78,6 +80,7 @@ const initialConfig: Config = {
'chunk'
),
showSelectedHeadings: parseBoolean('selected_headings', false),
strictMode: parseBoolean('strict', false),
}

const setSearchParams = (config: Config) => {
Expand All @@ -92,6 +95,7 @@ const setSearchParams = (config: Config) => {
'selected_headings',
config.showSelectedHeadings ? 'true' : 'false'
)
searchParams.set('strict', config.strictMode ? 'true' : 'false')
history.replaceState({}, '', `?${searchParams.toString()}`)
}
}
Expand Down Expand Up @@ -183,6 +187,26 @@ const HugeDocumentExample = () => {
[config.contentVisibilityMode, config.chunkOutlines]
)

const editable = rendering ? (
<div>Rendering&hellip;</div>
) : (
<Slate key={editorVersion} editor={editor} initialValue={initialValue}>
<Editable
placeholder="Enter some text…"
renderElement={renderElement}
renderChunk={config.chunkDivs ? renderChunk : undefined}
spellCheck
autoFocus
/>
</Slate>
)

const editableWithStrictMode = config.strictMode ? (
<StrictMode>{editable}</StrictMode>
) : (
editable
)

return (
<>
<PerformanceControls
Expand All @@ -191,19 +215,7 @@ const HugeDocumentExample = () => {
setConfig={setConfig}
/>

{rendering ? (
<div>Rendering&hellip;</div>
) : (
<Slate key={editorVersion} editor={editor} initialValue={initialValue}>
<Editable
placeholder="Enter some text…"
renderElement={renderElement}
renderChunk={config.chunkDivs ? renderChunk : undefined}
spellCheck
autoFocus
/>
</Slate>
)}
{editableWithStrictMode}
</>
)
}
Expand Down Expand Up @@ -483,6 +495,21 @@ const PerformanceControls = ({
Call <code>useSelected</code> in each heading
</label>
</p>

<p>
<label>
<input
type="checkbox"
checked={config.strictMode}
onChange={event =>
setConfig({
strictMode: event.target.checked,
})
}
/>{' '}
React strict mode (only works in localhost)
</label>
</p>
</details>

<details>
Expand Down
Loading