Skip to content

LukeBf86/react-pretext

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-pretext

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.


Installation

npm install react-pretext @chenglou/pretext

Peer dependencies: react >= 17, react-dom >= 17, @chenglou/pretext


Quick start

Hook

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>
  )
}

Component

import { PreText } from 'react-pretext'

function MyComponent() {
  return (
    <PreText
      fontSize={16}
      fontFamily="Inter"
      onLayout={({ height, lineCount }) => console.log(height, lineCount)}
    >
      Hello world
    </PreText>
  )
}

API

useTextLayout(text, options?)

function useTextLayout(text: string, options?: UseTextLayoutOptions): TextLayoutResult

Options

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.

Returns

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.

<PreText>

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(options)

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.


Types

type FontProps = {
  fontSize?: number
  fontFamily?: string
  fontWeight?: string | number
  fontStyle?: string
}

type LineInfo = {
  text: string
  width: number
  x: number
  y: number
}

Auto font detection

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".


SSR / Next.js

Default mode (renderLines={false}) — SSR safe

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.

renderLines={true} — client-only

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.


Demo

npm run demo

Opens a Vite dev server with five interactive examples:

  1. Basic <PreText> with auto font detection
  2. renderLines with custom line highlight
  3. useTextLayout hook with a manual width slider
  4. Live resize — drag the container edge to see layout update in real time
  5. Cache behaviour — edit text to observe prepare() call counts

License

MIT

About

React wrapper for PRetext

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors