refactor(files): cleanup anti-patterns across file viewer components

Six-pass cleanup over the file-viewer directory:

Effects (you-might-not-need-an-effect):
- AudioPreview, VideoPreview: replace reset useEffect with key={file.id} so
  the component remounts on file change — React's canonical solution
- DocxPreview: same key-prop fix; removes a 5-setState reset effect that was
  also clearing containerRef.current.innerHTML unnecessarily

Callbacks (you-might-not-need-a-callback):
- handleEditorMount, handleEditorChange: remove useCallback — MonacoEditor is
  dynamic(), not React.memo, so reference stability has no observer
- markSavedContent: remove useCallback — called only through an onSaveRef,
  never directly observed
- DataTable.setInputRef: remove useCallback — callback refs on native elements
  are called regardless of reference identity

Design tokens (emcn-design-review):
- VideoPreview: bg-black → bg-[var(--surface-inverted)]
- HtmlPreview iframe: bg-white → bg-[var(--surface-2)]

useMemo, useState, and react-query passes found no issues.
This commit is contained in:
waleed
2026-04-27 23:59:21 -07:00
parent a05542a4d2
commit 9f34a27981
3 changed files with 22 additions and 43 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import { forwardRef, memo, useCallback, useImperativeHandle, useRef, useState } from 'react'
import { forwardRef, memo, useImperativeHandle, useRef, useState } from 'react'
import { cn } from '@/lib/core/utils/cn'
interface EditConfig {
@@ -49,12 +49,12 @@ const DataTableBase = forwardRef<DataTableHandle, DataTableProps>(function DataT
[]
)
const setInputRef = useCallback((node: HTMLInputElement | null) => {
const setInputRef = (node: HTMLInputElement | null) => {
if (node) {
node.focus()
node.select()
}
}, [])
}
const startEdit = (row: number, col: number, currentValue: string) => {
if (!editConfig) return

View File

@@ -521,9 +521,9 @@ function useTextEditorContentState(options: SyncTextEditorContentStateOptions) {
dispatch({ type: 'edit', content })
}, [])
const markSavedContent = useCallback((content: string) => {
const markSavedContent = (content: string) => {
dispatch({ type: 'save-success', content })
}, [])
}
return {
content: state.content,
@@ -596,15 +596,22 @@ export function FileViewer({
}
if (category === 'audio-previewable') {
return <AudioPreview file={file} workspaceId={workspaceId} />
return <AudioPreview key={file.id} file={file} workspaceId={workspaceId} />
}
if (category === 'video-previewable') {
return <VideoPreview file={file} workspaceId={workspaceId} />
return <VideoPreview key={file.id} file={file} workspaceId={workspaceId} />
}
if (category === 'docx-previewable') {
return <DocxPreview file={file} workspaceId={workspaceId} streamingContent={streamingContent} />
return (
<DocxPreview
key={file.id}
file={file}
workspaceId={workspaceId}
streamingContent={streamingContent}
/>
)
}
if (category === 'pptx-previewable') {
@@ -704,7 +711,6 @@ function TextEditor({
})
contentRef.current = content
// Sync external content (initial load + streaming) to Monaco model
useEffect(() => {
const editor = monacoEditorRef.current
if (!editor) return
@@ -713,9 +719,6 @@ function TextEditor({
const monacoValue = model.getValue()
if (monacoValue === content) return
// Only override Monaco when we're pushing external content, not user edits:
// - During streaming/reconciling: always push
// - On first init (monacoValue matches last synced value): push
if (isStreamInteractionLocked || monacoValue === lastSyncedContentRef.current) {
model.setValue(content)
lastSyncedContentRef.current = content
@@ -825,7 +828,6 @@ function TextEditor({
const toggled = toggleMarkdownCheckbox(content, checkboxIndex, checked)
if (toggled !== content) {
setDraftContent(toggled)
// Also update Monaco synchronously so the user sees the change
const model = monacoEditorRef.current?.getModel()
if (model) {
model.setValue(toggled)
@@ -836,7 +838,7 @@ function TextEditor({
[content, setDraftContent]
)
const handleEditorMount: OnMount = useCallback((editor, monaco) => {
const handleEditorMount: OnMount = (editor, monaco) => {
monacoEditorRef.current = editor
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
@@ -854,14 +856,11 @@ function TextEditor({
hasAutoFocusedRef.current = true
editor.focus()
}
}, [])
}
const handleEditorChange = useCallback(
(value: string | undefined) => {
setDraftContent(value ?? '')
},
[setDraftContent]
)
const handleEditorChange = (value: string | undefined) => {
setDraftContent(value ?? '')
}
const isStreaming = isStreamInteractionLocked
const isEditorReadOnly = isStreamInteractionLocked || !canEdit
@@ -1221,10 +1220,6 @@ const AudioPreview = memo(function AudioPreview({
}
}, [])
useEffect(() => {
replaceBlobUrl(null)
}, [file.id, file.key, replaceBlobUrl])
useEffect(() => {
return () => {
if (blobUrlRef.current) {
@@ -1290,10 +1285,6 @@ const VideoPreview = memo(function VideoPreview({
}
}, [])
useEffect(() => {
replaceBlobUrl(null)
}, [file.id, file.key, replaceBlobUrl])
useEffect(() => {
return () => {
if (blobUrlRef.current) {
@@ -1320,7 +1311,7 @@ const VideoPreview = memo(function VideoPreview({
}
return (
<div className='flex h-full items-center justify-center bg-black'>
<div className='flex h-full items-center justify-center bg-[var(--surface-inverted)]'>
{blobUrl && (
// biome-ignore lint/a11y/useMediaCaption: video from workspace files
<video src={blobUrl} controls className='max-h-full max-w-full' />
@@ -1440,16 +1431,6 @@ const DocxPreview = memo(function DocxPreview({
const [rendering, setRendering] = useState(false)
const [hasRenderedPreview, setHasRenderedPreview] = useState(false)
useEffect(() => {
lastSuccessfulHtmlRef.current = ''
setRenderError(null)
setRendering(false)
setHasRenderedPreview(false)
if (containerRef.current) {
containerRef.current.innerHTML = ''
}
}, [file.id, file.key])
useEffect(() => {
if (!containerRef.current || !fileData || streamingContent !== undefined) return
@@ -2019,7 +2000,6 @@ const XlsxPreview = memo(function XlsxPreview({
)
const handleSave = useCallback(async () => {
// Commit any in-progress cell edit before reading the workbook
dataTableRef.current?.commitEdit()
const wb = workbookRef.current
if (!wb || isSavingRef.current) return
@@ -2033,7 +2013,6 @@ const XlsxPreview = memo(function XlsxPreview({
const binary: number[] = XLSX.write(wb, { type: 'array', bookType: 'xlsx' })
const bytes = new Uint8Array(binary)
// Convert to base64 in chunks to avoid call stack overflow
const chunkSize = 8192
const parts: string[] = []
for (let i = 0; i < bytes.length; i += chunkSize) {

View File

@@ -851,7 +851,7 @@ const HtmlPreview = memo(function HtmlPreview({ content }: { content: string })
sandbox='allow-scripts'
referrerPolicy='no-referrer'
title='HTML Preview'
className='h-full w-full border-0 bg-white'
className='h-full w-full border-0 bg-[var(--surface-2)]'
/>
)}
</div>