LiveReact

repository·main·Indexed 42 days ago

https://github.com/mrdotb/live_react

A library for embedding React components within Phoenix LiveView applications. It provides E2E reactivity, server-side rendering (SSR) via a Node.js worker, and a specialized wire format for incremental prop updates and Phoenix Streams support. Includes the useLiveReact hook for websocket communication, a LiveView-aware Link component, and a Vite plugin for HMR and SSR integration.

Tokens
22.5K
Snippets
78
Records
99
Agent score
52%

What's inside LiveReact

  1. Overview of LiveReact

    main

    LiveReact is a library that allows you to use React components inside Phoenix LiveView. It bridges the gap between server-rendered HTML and client-side state management by enabling React to communicate over the LiveView websocket.

    Key capabilities include:

    • End-To-End Reactivity: Seamless integration with LiveView state.
    • Incremental Prop Updates: Only changed data is sent over the wire to optimize performance.
    • Phoenix Streams Support: Use Phoenix Streams directly as props.
    • Form & Upload Encoding: Automatically handles changeset errors and upload states for React consumption.
    • Server-Side Rendering (SSR): Supports SSR for React components.
    • Advanced Features: Support for Tailwind, Dead Views, Lazy-loading, Slot interoperability, and a specialized Link component for LiveView navigation.
  2. How `LiveReact.Encoder` handles Ecto Changeset errors

    main

    When encoding Phoenix forms, LiveReact.Encoder can process Ecto.Changeset data to extract and translate errors into a format suitable for React.

    Key behaviors:

    • Error Translation: It uses translate_errors/1 to convert Ecto error tuples into human-readable strings by performing string interpolation on error messages (e.g., replacing %{count} with the actual value).
    • Nested Errors: It recursively collects errors for associations with cardinality :one or :many.
    • Form Integration: If the form implementation is Phoenix.HTML.FormData.Ecto.Changeset, it uses collect_changeset_values to gather data and collect_changeset_errors to gather errors from the underlying changeset.
    • Gettext Note: Currently, error translation relies on plain string interpolation and does not use a Gettext backend.
  3. How props diffing works in LiveReact 2.0

    main

    In LiveReact 2.0, instead of re-sending the entire props payload on every update, the library sends only the changes (patches). This is achieved using three data attributes on the component's wrapper element:

    • data-props: A full snapshot of the props, written on the initial render to allow clients to resynchronize upon reconnection.
    • data-props-diff: Patch operations describing what changed since the last render.
    • data-streams-diff: Patch operations specifically for stream assigns.

    The client hook uses a fourth attribute, data-use-diff, to determine whether to read from the full snapshot or the diff.

    Diffs are computed using LiveView's change tracking. For lists of maps, LiveReact matches items on their id field, meaning reordering or inserting items does not require re-sending untouched entries. The operations are serialized in a compact format defined by LiveReact.Patch.

  4. How props diffing works in LiveReact

    main

    LiveReact optimizes React component updates by calculating minimal JSON Patch operations for changed props. Instead of sending the entire props object on every update, it uses Phoenix LiveView's __changed__ tracking to identify specific changes.

    When a prop changes:

    1. If the prop is a simple value, a replace operation is generated.
    2. If the prop is a complex structure (like a map or struct), Jsonpatch.diff is used to calculate the granular changes within that structure.
    3. The resulting patches are serialized and passed to the frontend via the data-props-diff attribute on the component's container div.
    # Note: The implementation details are internal to `LiveReact.react/1`, but the consumer sees the result via `data-props-diff`.
  5. How Props Diffing and LiveView Streams work together

    main

    LiveReact uses two distinct mechanisms to minimize wire payload and optimize React re-renders:

    1. Props Diffing: Instead of re-sending the full data-props JSON on every update, the server computes a JSON-Patch-style diff using the Jsonpatch library and sends it via data-props-diff. On the client, these patches are applied using a copy-on-write strategy. This ensures that unaffected items in an array or object keep their original references, allowing React.memo to successfully skip re-renders for unchanged data.

    2. LiveView Streams: %Phoenix.LiveView.LiveStream{} assigns are automatically detected and routed to a streams map. Because streams do not hold a full snapshot server-side, they are communicated exclusively via data-streams-diff patches (handling reset?, deletes, and inserts). The client builds the stream state from scratch starting from an empty object {}.

    Client-side State Merging: On every update, the React component re-renders with a merged object containing both props and streams: {...this._props, ...this._streams, pushEvent, ...}.

  6. How LiveView Streams are diffed in LiveReact

    main

    LiveReact supports Phoenix.LiveView.LiveStream by calculating specific patch operations for stream updates. This allows the React frontend to efficiently sync with LiveView streams (insertions, deletions, resets, or limits) without re-rendering the entire list.

    Key patch operations for streams include:

    • replace: Used for initial renders or resetting a stream (e.g., {%{op: "replace", path: "/users", value: []}%}).
    • upsert: Used when inserting items into a stream (e.g., {%{op: "upsert", path: "/users/-", value: %{...}%}).
    • remove: Used when deleting an item (e.g., {%{op: "remove", path: "/users/$$users-2"}%}).
    • limit: Used when a stream limit is applied (e.g., {%{op: "limit", path: "/users", value: 5}%}).

    Stream diffs are provided to the React component via the data-streams-diff attribute.

    # Example of expected stream patch shapes:
    # Insertion:
    %{"op" => "upsert", "path" => "/users/-", "value" => %{"id" => 3, "name" => "Charlie", "age" => 28, "__dom_id" => "users-3"}}
    
    # Deletion:
    %{"op" => "remove", "path" => "/users/$$users-2"}
    
    # Reset:
    %{"op" => "replace", "path" => "/users", "value" => []}
  7. Use Phoenix LiveView Streams with React components

    main

    LiveReact supports Phoenix.LiveView.stream/3,4 assigns. When you pass a value of type %Phoenix.LiveView.LiveStream{} as a prop to a <.react /> component, LiveReact automatically handles it as a stream, delivering updates over data-streams-diff instead of re-sending the entire list.

    To use this, ensure your LiveView uses stream/3 or stream/4 to manage the data and pass the stream assign to the component.

    # In your LiveView
    def mount(_params, _session, socket) do
      socket = 
        socket
        |> assign(:next_id, 1)
        |> stream(:messages, [%{id: 0, text: "Welcome!"}])
      {:ok, socket}
    end
    
    # In your render/template
    <.react name="StreamDemo" messages={@streams.messages} socket={@socket} />
    // In your React component
    export default function StreamDemo({ messages, pushEvent }) {
      return (
        <ul>
          {messages.map((message) => (
            <li key={message.__dom_id}>
              {message.text}
            </li>
          ))}
        </ul>
      );
    }
  8. How Props Diffing and LiveView Streams work in LiveReact

    main

    LiveReact implements a compact wire format to send only changes (diffs) to the client instead of full JSON payloads. This is achieved through two main mechanisms:

    1. Props Diffing: The server uses LiveReact.Encoder to normalize structs into maps and Jsonpatch.diff/3 to compute differences. These are serialized via LiveReact.Patch into data-props-diff attributes.
    2. LiveView Streams: Data from Phoenix.LiveView.stream/3,4 is processed by hand-rolled patch generation that reads %Phoenix.LiveView.LiveStream{} operations directly, serialized into data-streams-diff attributes.

    Client-side behavior: The React hook maintains persistent _props and _streams state per component instance. It applies patches using a copy-on-write strategy: only the top-level value at each patch's first path segment receives a new reference. This ensures that React.memo'd children correctly skip re-rendering for untouched items while re-rendering only when necessary.

  9. Classify LiveStream values as :streams instead of :props

    main

    By default, LiveReact.react/1 extracts assigns into data-props. However, Phoenix.LiveView.LiveStream values should not be treated as standard props because they are not JSON-encodable in the same way.

    LiveReact uses a normalize_key/2 function to classify assigns. When a value is a %Phoenix.LiveView.LiveStream{} struct, it is classified as :streams. This ensures that stream data is excluded from the data-props attribute, preventing encoding errors and allowing streams to be handled via the specialized stream mechanism.

  10. Understand the LiveReact.Patch wire format

    main

    The LiveReact.Patch module encodes patch operations into a compact binary format used for data-props-diff and data-streams-diff attributes. This format is optimized for transport via HTML attributes.

    It uses a concatenated sequence of operations where dynamic text fields (paths and values) are prefixed with their JavaScript string length. This allows paths and values to contain delimiters without requiring complex escaping.

    Operation Codes

    CodeOperationDescription
    aaddAdds a new value
    dremoveRemoves a value (omits <value>)
    rreplaceReplaces an existing value
    uupsertUpserts a value (used for streams)
    llimitSets a limit on a collection
    nnonceA marker used to force LiveView to send an attribute change; ignored during decoding

    Value Tags

    TagValue TypeDescription
    znilRepresents a null value
    b0booleanRepresents false
    b1booleanRepresents true
    n<len>:<number>numberInteger or float
    s<len>:<string>stringUTF-8 string
    J<len>:<json>complexMaps, lists, and other complex JSON structures (caret-encoded)
  11. Configure Dockerfile for LiveReact with SSR

    main

    For SSR deployments, nodejs must be installed in both the build stage and the final runtime stage. Modify your ./Dockerfile as follows:

    1. Build Stage: Add curl to dependencies, install nodejs (19.x), and run npm install in /app/assets.
    2. Runtime Stage: In the stage starting with FROM ${RUNNER_IMAGE}, add curl to the apt-get install list and install nodejs via the Nodesource setup script.
    # ./Dockerfile
    
    ...
    
    # install build dependencies
    - RUN apt-get update -y && apt-get install -y build-essential git \
    + RUN apt-get update -y && apt-get install -y build-essential git curl \
        && apt-get clean && rm -f /var/lib/apt/lists/*_*
    
    + # install nodejs for build stage
    + RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash - && apt-get install -y nodejs
    
    ...
    
    COPY assets assets
    
    + # install all npm packages in assets directory
    + RUN cd /app/assets && npm install
    
    ...
    
    # start a new build stage so that the final image will only contain
    # the compiled release and other runtime necessities
    FROM ${RUNNER_IMAGE}
    
    RUN apt-get update -y && \
    -  apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
    +  apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates curl \
       && apt-get clean && rm -f /var/lib/apt/lists/*_*
    
    + # install nodejs for production environment
    + RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash - && apt-get install -y nodejs
    
    ...