Skip to content

Commit a4e6e4d

Browse files
authored
fix(highlighter): clean up stale rough-notation instances on prop changes (#929)
1 parent 5cc7048 commit a4e6e4d

4 files changed

Lines changed: 29 additions & 41 deletions

File tree

apps/www/public/llms-full.txt

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8023,7 +8023,7 @@ Description: A text highlighter that mimics the effect of a human-drawn marker s
80238023
--- file: magicui/highlighter.tsx ---
80248024
"use client"
80258025

8026-
import { useEffect, useRef } from "react"
8026+
import { useLayoutEffect, useRef } from "react"
80278027
import type React from "react"
80288028
import { useInView } from "motion/react"
80298029
import { annotate } from "rough-notation"
@@ -8062,7 +8062,6 @@ export function Highlighter({
80628062
isView = false,
80638063
}: HighlighterProps) {
80648064
const elementRef = useRef<HTMLSpanElement>(null)
8065-
const annotationRef = useRef<RoughAnnotation | null>(null)
80668065

80678066
const isInView = useInView(elementRef, {
80688067
once: true,
@@ -8072,8 +8071,9 @@ export function Highlighter({
80728071
// If isView is false, always show. If isView is true, wait for inView
80738072
const shouldShow = !isView || isInView
80748073

8075-
useEffect(() => {
8074+
useLayoutEffect(() => {
80768075
const element = elementRef.current
8076+
let annotation: RoughAnnotation | null = null
80778077
let resizeObserver: ResizeObserver | null = null
80788078

80798079
if (shouldShow && element) {
@@ -8087,25 +8087,21 @@ export function Highlighter({
80878087
multiline,
80888088
}
80898089

8090-
const annotation = annotate(element, annotationConfig)
8091-
8092-
annotationRef.current = annotation
8093-
annotation.show()
8090+
const currentAnnotation = annotate(element, annotationConfig)
8091+
annotation = currentAnnotation
8092+
currentAnnotation.show()
80948093

80958094
resizeObserver = new ResizeObserver(() => {
8096-
annotation.hide()
8097-
annotation.show()
8095+
currentAnnotation.hide()
8096+
currentAnnotation.show()
80988097
})
80998098

81008099
resizeObserver.observe(element)
81018100
resizeObserver.observe(document.body)
81028101
}
81038102

81048103
return () => {
8105-
if (annotationRef.current) {
8106-
annotationRef.current.remove()
8107-
annotationRef.current = null
8108-
}
8104+
annotation?.remove()
81098105
if (resizeObserver) {
81108106
resizeObserver.disconnect()
81118107
}
@@ -8156,7 +8152,7 @@ export default function HighlighterDemo() {
81568152
--- file: magicui/highlighter.tsx ---
81578153
"use client"
81588154

8159-
import { useEffect, useRef } from "react"
8155+
import { useLayoutEffect, useRef } from "react"
81608156
import type React from "react"
81618157
import { useInView } from "motion/react"
81628158
import { annotate } from "rough-notation"
@@ -8195,7 +8191,6 @@ export function Highlighter({
81958191
isView = false,
81968192
}: HighlighterProps) {
81978193
const elementRef = useRef<HTMLSpanElement>(null)
8198-
const annotationRef = useRef<RoughAnnotation | null>(null)
81998194

82008195
const isInView = useInView(elementRef, {
82018196
once: true,
@@ -8205,8 +8200,9 @@ export function Highlighter({
82058200
// If isView is false, always show. If isView is true, wait for inView
82068201
const shouldShow = !isView || isInView
82078202

8208-
useEffect(() => {
8203+
useLayoutEffect(() => {
82098204
const element = elementRef.current
8205+
let annotation: RoughAnnotation | null = null
82108206
let resizeObserver: ResizeObserver | null = null
82118207

82128208
if (shouldShow && element) {
@@ -8220,25 +8216,21 @@ export function Highlighter({
82208216
multiline,
82218217
}
82228218

8223-
const annotation = annotate(element, annotationConfig)
8224-
8225-
annotationRef.current = annotation
8226-
annotation.show()
8219+
const currentAnnotation = annotate(element, annotationConfig)
8220+
annotation = currentAnnotation
8221+
currentAnnotation.show()
82278222

82288223
resizeObserver = new ResizeObserver(() => {
8229-
annotation.hide()
8230-
annotation.show()
8224+
currentAnnotation.hide()
8225+
currentAnnotation.show()
82318226
})
82328227

82338228
resizeObserver.observe(element)
82348229
resizeObserver.observe(document.body)
82358230
}
82368231

82378232
return () => {
8238-
if (annotationRef.current) {
8239-
annotationRef.current.remove()
8240-
annotationRef.current = null
8241-
}
8233+
annotation?.remove()
82428234
if (resizeObserver) {
82438235
resizeObserver.disconnect()
82448236
}

apps/www/public/r/highlighter-demo.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
{
2020
"path": "registry/magicui/highlighter.tsx",
21-
"content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport type React from \"react\"\nimport { useInView } from \"motion/react\"\nimport { annotate } from \"rough-notation\"\nimport { type RoughAnnotation } from \"rough-notation/lib/model\"\n\ntype AnnotationAction =\n | \"highlight\"\n | \"underline\"\n | \"box\"\n | \"circle\"\n | \"strike-through\"\n | \"crossed-off\"\n | \"bracket\"\n\ninterface HighlighterProps {\n children: React.ReactNode\n action?: AnnotationAction\n color?: string\n strokeWidth?: number\n animationDuration?: number\n iterations?: number\n padding?: number\n multiline?: boolean\n isView?: boolean\n}\n\nexport function Highlighter({\n children,\n action = \"highlight\",\n color = \"#ffd1dc\",\n strokeWidth = 1.5,\n animationDuration = 600,\n iterations = 2,\n padding = 2,\n multiline = true,\n isView = false,\n}: HighlighterProps) {\n const elementRef = useRef<HTMLSpanElement>(null)\n const annotationRef = useRef<RoughAnnotation | null>(null)\n\n const isInView = useInView(elementRef, {\n once: true,\n margin: \"-10%\",\n })\n\n // If isView is false, always show. If isView is true, wait for inView\n const shouldShow = !isView || isInView\n\n useEffect(() => {\n const element = elementRef.current\n let resizeObserver: ResizeObserver | null = null\n\n if (shouldShow && element) {\n const annotationConfig = {\n type: action,\n color,\n strokeWidth,\n animationDuration,\n iterations,\n padding,\n multiline,\n }\n\n const annotation = annotate(element, annotationConfig)\n\n annotationRef.current = annotation\n annotation.show()\n\n resizeObserver = new ResizeObserver(() => {\n annotation.hide()\n annotation.show()\n })\n\n resizeObserver.observe(element)\n resizeObserver.observe(document.body)\n }\n\n return () => {\n if (annotationRef.current) {\n annotationRef.current.remove()\n annotationRef.current = null\n }\n if (resizeObserver) {\n resizeObserver.disconnect()\n }\n }\n }, [\n shouldShow,\n action,\n color,\n strokeWidth,\n animationDuration,\n iterations,\n padding,\n multiline,\n ])\n\n return (\n <span ref={elementRef} className=\"relative inline-block bg-transparent\">\n {children}\n </span>\n )\n}\n",
21+
"content": "\"use client\"\n\nimport { useLayoutEffect, useRef } from \"react\"\nimport type React from \"react\"\nimport { useInView } from \"motion/react\"\nimport { annotate } from \"rough-notation\"\nimport { type RoughAnnotation } from \"rough-notation/lib/model\"\n\ntype AnnotationAction =\n | \"highlight\"\n | \"underline\"\n | \"box\"\n | \"circle\"\n | \"strike-through\"\n | \"crossed-off\"\n | \"bracket\"\n\ninterface HighlighterProps {\n children: React.ReactNode\n action?: AnnotationAction\n color?: string\n strokeWidth?: number\n animationDuration?: number\n iterations?: number\n padding?: number\n multiline?: boolean\n isView?: boolean\n}\n\nexport function Highlighter({\n children,\n action = \"highlight\",\n color = \"#ffd1dc\",\n strokeWidth = 1.5,\n animationDuration = 600,\n iterations = 2,\n padding = 2,\n multiline = true,\n isView = false,\n}: HighlighterProps) {\n const elementRef = useRef<HTMLSpanElement>(null)\n\n const isInView = useInView(elementRef, {\n once: true,\n margin: \"-10%\",\n })\n\n // If isView is false, always show. If isView is true, wait for inView\n const shouldShow = !isView || isInView\n\n useLayoutEffect(() => {\n const element = elementRef.current\n let annotation: RoughAnnotation | null = null\n let resizeObserver: ResizeObserver | null = null\n\n if (shouldShow && element) {\n const annotationConfig = {\n type: action,\n color,\n strokeWidth,\n animationDuration,\n iterations,\n padding,\n multiline,\n }\n\n const currentAnnotation = annotate(element, annotationConfig)\n annotation = currentAnnotation\n currentAnnotation.show()\n\n resizeObserver = new ResizeObserver(() => {\n currentAnnotation.hide()\n currentAnnotation.show()\n })\n\n resizeObserver.observe(element)\n resizeObserver.observe(document.body)\n }\n\n return () => {\n annotation?.remove()\n if (resizeObserver) {\n resizeObserver.disconnect()\n }\n }\n }, [\n shouldShow,\n action,\n color,\n strokeWidth,\n animationDuration,\n iterations,\n padding,\n multiline,\n ])\n\n return (\n <span ref={elementRef} className=\"relative inline-block bg-transparent\">\n {children}\n </span>\n )\n}\n",
2222
"type": "registry:ui"
2323
}
2424
]

apps/www/public/r/highlighter.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"files": [
1111
{
1212
"path": "registry/magicui/highlighter.tsx",
13-
"content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport type React from \"react\"\nimport { useInView } from \"motion/react\"\nimport { annotate } from \"rough-notation\"\nimport { type RoughAnnotation } from \"rough-notation/lib/model\"\n\ntype AnnotationAction =\n | \"highlight\"\n | \"underline\"\n | \"box\"\n | \"circle\"\n | \"strike-through\"\n | \"crossed-off\"\n | \"bracket\"\n\ninterface HighlighterProps {\n children: React.ReactNode\n action?: AnnotationAction\n color?: string\n strokeWidth?: number\n animationDuration?: number\n iterations?: number\n padding?: number\n multiline?: boolean\n isView?: boolean\n}\n\nexport function Highlighter({\n children,\n action = \"highlight\",\n color = \"#ffd1dc\",\n strokeWidth = 1.5,\n animationDuration = 600,\n iterations = 2,\n padding = 2,\n multiline = true,\n isView = false,\n}: HighlighterProps) {\n const elementRef = useRef<HTMLSpanElement>(null)\n const annotationRef = useRef<RoughAnnotation | null>(null)\n\n const isInView = useInView(elementRef, {\n once: true,\n margin: \"-10%\",\n })\n\n // If isView is false, always show. If isView is true, wait for inView\n const shouldShow = !isView || isInView\n\n useEffect(() => {\n const element = elementRef.current\n let resizeObserver: ResizeObserver | null = null\n\n if (shouldShow && element) {\n const annotationConfig = {\n type: action,\n color,\n strokeWidth,\n animationDuration,\n iterations,\n padding,\n multiline,\n }\n\n const annotation = annotate(element, annotationConfig)\n\n annotationRef.current = annotation\n annotation.show()\n\n resizeObserver = new ResizeObserver(() => {\n annotation.hide()\n annotation.show()\n })\n\n resizeObserver.observe(element)\n resizeObserver.observe(document.body)\n }\n\n return () => {\n if (annotationRef.current) {\n annotationRef.current.remove()\n annotationRef.current = null\n }\n if (resizeObserver) {\n resizeObserver.disconnect()\n }\n }\n }, [\n shouldShow,\n action,\n color,\n strokeWidth,\n animationDuration,\n iterations,\n padding,\n multiline,\n ])\n\n return (\n <span ref={elementRef} className=\"relative inline-block bg-transparent\">\n {children}\n </span>\n )\n}\n",
13+
"content": "\"use client\"\n\nimport { useLayoutEffect, useRef } from \"react\"\nimport type React from \"react\"\nimport { useInView } from \"motion/react\"\nimport { annotate } from \"rough-notation\"\nimport { type RoughAnnotation } from \"rough-notation/lib/model\"\n\ntype AnnotationAction =\n | \"highlight\"\n | \"underline\"\n | \"box\"\n | \"circle\"\n | \"strike-through\"\n | \"crossed-off\"\n | \"bracket\"\n\ninterface HighlighterProps {\n children: React.ReactNode\n action?: AnnotationAction\n color?: string\n strokeWidth?: number\n animationDuration?: number\n iterations?: number\n padding?: number\n multiline?: boolean\n isView?: boolean\n}\n\nexport function Highlighter({\n children,\n action = \"highlight\",\n color = \"#ffd1dc\",\n strokeWidth = 1.5,\n animationDuration = 600,\n iterations = 2,\n padding = 2,\n multiline = true,\n isView = false,\n}: HighlighterProps) {\n const elementRef = useRef<HTMLSpanElement>(null)\n\n const isInView = useInView(elementRef, {\n once: true,\n margin: \"-10%\",\n })\n\n // If isView is false, always show. If isView is true, wait for inView\n const shouldShow = !isView || isInView\n\n useLayoutEffect(() => {\n const element = elementRef.current\n let annotation: RoughAnnotation | null = null\n let resizeObserver: ResizeObserver | null = null\n\n if (shouldShow && element) {\n const annotationConfig = {\n type: action,\n color,\n strokeWidth,\n animationDuration,\n iterations,\n padding,\n multiline,\n }\n\n const currentAnnotation = annotate(element, annotationConfig)\n annotation = currentAnnotation\n currentAnnotation.show()\n\n resizeObserver = new ResizeObserver(() => {\n currentAnnotation.hide()\n currentAnnotation.show()\n })\n\n resizeObserver.observe(element)\n resizeObserver.observe(document.body)\n }\n\n return () => {\n annotation?.remove()\n if (resizeObserver) {\n resizeObserver.disconnect()\n }\n }\n }, [\n shouldShow,\n action,\n color,\n strokeWidth,\n animationDuration,\n iterations,\n padding,\n multiline,\n ])\n\n return (\n <span ref={elementRef} className=\"relative inline-block bg-transparent\">\n {children}\n </span>\n )\n}\n",
1414
"type": "registry:ui"
1515
}
1616
]

apps/www/registry/magicui/highlighter.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import { useEffect, useRef } from "react"
3+
import { useLayoutEffect, useRef } from "react"
44
import type React from "react"
55
import { useInView } from "motion/react"
66
import { annotate } from "rough-notation"
@@ -39,7 +39,6 @@ export function Highlighter({
3939
isView = false,
4040
}: HighlighterProps) {
4141
const elementRef = useRef<HTMLSpanElement>(null)
42-
const annotationRef = useRef<RoughAnnotation | null>(null)
4342

4443
const isInView = useInView(elementRef, {
4544
once: true,
@@ -49,8 +48,9 @@ export function Highlighter({
4948
// If isView is false, always show. If isView is true, wait for inView
5049
const shouldShow = !isView || isInView
5150

52-
useEffect(() => {
51+
useLayoutEffect(() => {
5352
const element = elementRef.current
53+
let annotation: RoughAnnotation | null = null
5454
let resizeObserver: ResizeObserver | null = null
5555

5656
if (shouldShow && element) {
@@ -64,25 +64,21 @@ export function Highlighter({
6464
multiline,
6565
}
6666

67-
const annotation = annotate(element, annotationConfig)
68-
69-
annotationRef.current = annotation
70-
annotation.show()
67+
const currentAnnotation = annotate(element, annotationConfig)
68+
annotation = currentAnnotation
69+
currentAnnotation.show()
7170

7271
resizeObserver = new ResizeObserver(() => {
73-
annotation.hide()
74-
annotation.show()
72+
currentAnnotation.hide()
73+
currentAnnotation.show()
7574
})
7675

7776
resizeObserver.observe(element)
7877
resizeObserver.observe(document.body)
7978
}
8079

8180
return () => {
82-
if (annotationRef.current) {
83-
annotationRef.current.remove()
84-
annotationRef.current = null
85-
}
81+
annotation?.remove()
8682
if (resizeObserver) {
8783
resizeObserver.disconnect()
8884
}

0 commit comments

Comments
 (0)