Skip to content

Latest commit

 

History

History
252 lines (208 loc) · 9.46 KB

react.md

File metadata and controls

252 lines (208 loc) · 9.46 KB

@ritojs/react

@ritojs/react is the React integration layer on top of the root reader from @ritojs/core and the controller layer from @ritojs/kit.

Use it when you want hooks for reader lifecycle and state, plus a mount component for the controller-managed reading surface.

Main Exports

import { useRitoReader, Reader } from '@ritojs/react';

Hooks:

  • useRitoReader
  • useRitoScrollReader
  • useRitoScrollView
  • useSelection
  • useScrollSelectionHandles
  • useSearch
  • useAnnotations
  • useReadingPosition
  • useContainerSize
  • useControllerEvent

ScrollSelectionHandles({ controller, rootRef, touchInput }) draws a scrolled view's selection handles as Reader draws the page controller's, placed over the view's canvas inside rootRef and moved with the strip. useScrollSelectionHandles(controller) gives those ends where the view shows them (start, end, and focusEdge naming the focus), following the strip and null while nothing is selected or a gesture still selects: what a toolbar over a scrolled selection stands on, as useSelection's focusRect is for pages.

Types:

  • UseRitoReaderOptions
  • RitoReaderState
  • RitoReaderActions
  • UseRitoScrollReaderOptions
  • RitoScrollReaderState
  • RitoScrollReaderActions
  • UseRitoScrollViewOptions
  • RitoScrollViewState
  • RitoScrollViewActions
  • SelectionState
  • SearchState
  • AnnotationsState
  • ReadingPositionState
  • ContainerSize

Components:

  • Reader
  • ReaderProps

Typical Use

import { useCallback, useEffect, useRef } from 'react';
import {
  Reader,
  useContainerSize,
  useControllerEvent,
  useReaderAutoResize,
  useRitoReader,
} from '@ritojs/react';
import { springPresets } from '@ritojs/kit';
import { pinnedFontPolicy } from './pinned-fonts'; // app-owned font bytes + digests

export function App() {
  const [sizeRef, containerSize] = useContainerSize();
  const { controller, error, load } = useRitoReader({
    reader: {
      width: Math.max(containerSize.width, 1),
      height: Math.max(containerSize.height, 1),
      margin: 40,
      spread: 'double',
      // Required: the engine shapes with host font bytes only.
      pinnedFontPolicy,
      // The book opens already styled: one layout, not one per setting.
      fontSize: 18,
      fontFamily: 'serif',
    },
    controller: {
      transition: { ...springPresets.default, effect: 'slide' },
    },
  });
  // Re-paginates on every container size change after the book is loaded.
  const resizeRef = useReaderAutoResize(controller, { margin: 40 });
  const containerRef = useCallback(
    (node: HTMLElement | null) => {
      sizeRef(node);
      resizeRef(node);
    },
    [resizeRef, sizeRef],
  );

  const didLoadRef = useRef(false);
  useEffect(() => {
    if (containerSize.width === 0 || containerSize.height === 0) return;
    if (didLoadRef.current) return;
    didLoadRef.current = true;
    void load(fetch('/book.epub').then((resp) => resp.arrayBuffer()));
  }, [containerSize.width, containerSize.height, load]);

  // Non-fatal: the book stays open (a font no face answers to, a failed search…).
  useControllerEvent(controller, 'error', ({ message, source }) => {
    console.warn(source, message);
  });

  return (
    <div ref={containerRef} style={{ width: '100vw', height: '100vh' }}>
      {error ? <p role="alert">{error}</p> : <Reader controller={controller} />}
    </div>
  );
}

useRitoReader

This is the highest-level React hook. It manages:

  • canvas creation
  • createReader()
  • createController()
  • state synchronization for spread count and active spread
  • cleanup on unmount

Use this hook when you want a full reader lifecycle in React.

Important behavior:

  • rendering the hook itself is SSR-safe
  • spreads is the navigation record, { index, pageIndexes, leftPageIndex, rightPageIndex? }, and totalSpreads its length; both are book-wide: empty and 0 until the whole book's layout first commits, and held at their last book-wide values while a chapter is committed alone after a relayout
  • load() still needs a browser document and should run in an effect or event handler
  • replacement load() calls wait for the previous Reader's engine session to be released before opening the next document
  • load() is separate from later responsive resizes: useReaderAutoResize(controller) calls controller.resize() whenever the observed element changes size (or call it yourself)
  • everything a book should open with — size, spread, theme, typography — belongs in options.reader, so the book lays out once, already styled; later changes go through the controller (setTypography, setSpreadMode, setTheme, setRenderScale)
  • error says why the book could not be opened. The controller's own error events are non-fatal and never land there: subscribe with useControllerEvent(controller, 'error', …)

Pinned fallback policy

useRitoReader() does not choose or fetch fallback fonts. A ReaderPinnedFontPolicy is required by the core: finish loading the app-owned static font bytes before calling load(), then pass the completed policy under options.reader.pinnedFontPolicy. The hook reads the current Reader options once the data given to load() has resolved; a missing or empty policy makes the load fail.

Replacing the policy object after load() does not reconfigure the open Reader. To add a face while a book is open (a system font, a file the reader imported), use controller.addFontFaces(); controller.fonts() lists every family a font-family choice can name, which is what a font picker should be built from. See Fonts for how families resolve.

useRitoScrollReader

useRitoScrollReader({ reader, scroll, initialLocator }) owns a scrolled reader (the book laid out with spread: 'scrolled' whatever reader.spread says) and its kit scroll view. Pass containerRef as the ref of the element the view is shown in; load(data) opens a book, letting go of the one before, and shows initialLocator's line at the reading line once the book is laid out; unload() lets it go. The state follows the view: progress (0 to 1 through the book), position (the locator at the reading line as of the strip's last rest, to save and reopen at) and selection (the selection event while something is selected, null otherwise), with controller for everything else the view does (scrollToLocator, setHighlights, on('tap')).

const { containerRef, load, progress, controller } = useRitoScrollReader({
  reader: { width, height, pinnedFontPolicy },
  initialLocator: saved,
});
useEffect(
  () =>
    controller?.on('tap', ({ target }) => {
      if (target?.targetLocator) void controller.scrollToLocator(target.targetLocator);
    }),
  [controller],
);
return <div ref={containerRef} style={{ width, height }} />;

useRitoScrollView

useRitoScrollView(controller, { active, paged, scroll }) reads the book a page controller shows scrolled, on the same reader: while active, the controller hands the reader over (suspend()), the reader is laid out scrolled and a scroll view shows it from where the pages were; turned off, the reader is paged again as paged says ('single' or 'double') from where the view was read, and the controller takes it back (resume()). Size, theme and typography reach the reader through the controller as before, and the view follows each layout. The state is the view's (controller, progress, position, selection), with isLoading while the scrolled layout is made (only after a short delay) and error; pass containerRef as the ref of the element the view is shown in. When the controller is replaced (another book opened), the view is let go with it.

const rito = useRitoReader({ reader: { width, height, pinnedFontPolicy } });
const scroll = useRitoScrollView(rito.controller, { active: scrolled, paged: 'double' });
return scrolled ? <div ref={scroll.containerRef} /> : <Reader controller={rito.controller} />;

useReaderTransition

useReaderTransition(controller) returns { isTransitioning, direction } from the controller's transitionStart / transitionEnd, for hiding chrome or deferring work while a page moves. Every start is answered by exactly one end, so it never sticks, and it resets when the controller is replaced.

useReaderTransitionProgress(controller) returns how far the turn on screen has moved, 0 at rest and 1 on arrival, from transitionProgress. It re-renders on every painted frame of a turn, so read it in a small component of its own (a scrim, a progress edge).

Reader

The Reader component mounts the controller's managed DOM surface into a container.

It does not own pagination logic itself, and it does not call controller.resize() for you. The controller remains the source of truth.

useSelection

Use selection.hasSelection as the selection-presence signal. selection.sourceLocator carries the durable source range when both endpoints share a resource and selection.sourceSpan the resource-qualified endpoints, while text, viewportRects, focusRect, and handles are ready for copy and selection UI. The hook clears all selection state when its controller is replaced so data cannot leak between books.

Guidance

  • Use @ritojs/react if you want fast app integration and React state bindings.
  • Use @ritojs/kit directly if you want non-React UI or a custom state layer.
  • Use @ritojs/core if you only need the core reader without React state wiring.

Related Docs