React wrapper for @chenglou/pretext — fast multiline text measurement and layout without DOM reflow.
pretext separates text measurement into two phases:
prepare(text, font)— segments and measures text using an off-screen canvas (~expensive, cached)layout(handle, width, lineHeight)— pure arithmetic, sub-millisecond per call
react-pretext wraps these phases behind a hook and a component, adds automatic width tracking via ResizeObserver, reads font properties from getComputedStyle when not provided explicitly, and caches prepare() handles in a global LRU cache.
npm install react-pretext @chenglou/pretextPeer dependencies: react >= 17, react-dom >= 17, @chenglou/pretext
import { useTextLayout } from 'react-pretext'
function MyComponent() {
const { ref, height, lineCount } = useTextLayout('Hello world', {
width: 320,
fontSize: 16,
fontFamily: 'Inter',
})
return (
<div ref={ref as React.RefObject<HTMLDivElement>} style={{ width: 320, fontSize: 16 }}>
Hello world
<span>height: {height}px, lines: {lineCount}</span>
</div>
)
}import { PreText } from 'react-pretext'
function MyComponent() {
return (
<PreText
fontSize={16}
fontFamily="Inter"
onLayout={({ height, lineCount }) => console.log(height, lineCount)}
>
Hello world
</PreText>
)
}function useTextLayout(text: string, options?: UseTextLayoutOptions): TextLayoutResult| Prop | Type | Default | Description |
|---|---|---|---|
width |
number |
— | Container width in px. If omitted, tracked via ResizeObserver on the returned ref. |
lineHeight |
number |
fontSize * 1.2 |
Line height in px. |
withLines |
boolean |
false |
When true, populates lines via layoutWithLines(). |
fontSize |
number |
from getComputedStyle |
|
fontFamily |
string |
from getComputedStyle |
|
fontWeight |
string | number |
from getComputedStyle |
|
fontStyle |
string |
from getComputedStyle |
|
cacheSize |
number |
500 |
Override global LRU max size. See configureCache() for deterministic control. |
| Field | Type | Description |
|---|---|---|
ref |
RefObject<HTMLElement> |
Attach to the container element. Used for auto-width and font fallback. |
height |
number | null |
null until first measurement (always null during SSR). |
lineCount |
number | null |
null until first measurement. |
lines |
LineInfo[] | null |
Populated only when withLines: true. |
All FontProps fields are available as props, plus width and lineHeight from UseTextLayoutOptions, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
children |
string |
required | The text to measure and render. |
renderLines |
boolean |
false |
Render each line as an absolutely positioned <span>. Client-only — see SSR section. |
onLayout |
(info: { height, lineCount, lines? }) => void |
— | Called after each measurement update. |
as |
keyof JSX.IntrinsicElements |
"div" |
HTML element to render as. |
All other HTML attributes are forwarded to the wrapper element.
configureCache({ maxSize: 1000 })Sets the global LRU cache capacity. Call once at app startup for deterministic behaviour. The default capacity is 500 entries. Entries are keyed on text + fontString and evicted least-recently-used.
type FontProps = {
fontSize?: number
fontFamily?: string
fontWeight?: string | number
fontStyle?: string
}
type LineInfo = {
text: string
width: number
x: number
y: number
}When FontProps fields are omitted, react-pretext reads them from getComputedStyle on the element attached to ref. This means you can style the element via CSS and skip passing font props entirely:
<PreText style={{ fontSize: '1rem', fontFamily: 'Inter' }} onLayout={console.log}>
Hello world
</PreText>If the element is not yet mounted (edge case on first render), the fallback is "normal normal 16px sans-serif".
height and lineCount are null on the server. The component renders its children inside the wrapper element without layout data. No crash, no hydration mismatch. After hydration, the hook activates and onLayout fires with real values.
On the server there are no positioned <span> elements. After hydration, absolutely positioned spans appear. This causes a React hydration mismatch warning.
Next.js App Router — mark as client component:
// components/MyText.tsx
'use client'
import { PreText } from 'react-pretext'
export function MyText({ children }: { children: string }) {
return <PreText renderLines fontSize={16} fontFamily="Inter">{children}</PreText>
}Next.js Pages Router — use dynamic import:
import dynamic from 'next/dynamic'
const PreText = dynamic(
() => import('react-pretext').then((m) => m.PreText),
{ ssr: false },
)When renderLines={true} and height is not yet known (before the first client-side measurement), the wrapper has height: 0 and spans are not visible. This resolves after the first layout cycle on the client.
npm run demoOpens a Vite dev server with five interactive examples:
- Basic
<PreText>with auto font detection renderLineswith custom line highlightuseTextLayouthook with a manual width slider- Live resize — drag the container edge to see layout update in real time
- Cache behaviour — edit text to observe
prepare()call counts
MIT