Tutorial,  Software,  Engineering

PayloadCMS - How to render Lexical Editor content in React Native?

Author

joprocorp

Date Published

This package is available in NPM under @lizardglobal/payload-richtext-lexical-react-native .

In the past few months, I've had the opportunity to learn and work with the PayloadCMS, a content management system which I quite like for its flexibility and familiarity. I have to say, I haven't worked with many CMS before, so it is difficult for me to make appropriate comparisons, and maybe this could be the topic of another post in the future. So, for now, I'd like to refrain from making comments about the good or bad of Payload, since I don't have a good comparison basis, and talk about a, perhaps intentional, gap in the feature coverage of the CMS.

If you’ve spent any time in the Payload ecosystem lately, you’ve likely encountered their Rich Text field. While they previously leaned on Slate, the shift to the Lexical editor has been a game-changer. Lexical, originally built by the team at Meta, is less of a "plug-in" and more of a high-performance framework. It stores content as a beautifully structured JSON tree rather than a messy string of HTML. For developers, this is a dream: it means your content is predictable, type-safe, and theoretically "portable" to any platform.

The gap

Payload's Lexical editor is a dream for web developers. It’s extensible, produces clean JSON, and the official React renderer makes displaying content on a website feel like a "set it and forget it" task.

However, if you’re building a cross-platform ecosystem such as a web dashboard and a React Native mobile app, that "easy" button disappears. React Native doesn’t dynamically work with plain HTML, and it certainly doesn't know what a <div> or a <span> is. While Payload provides a robust React renderer, there isn't an official, plug-and-play solution for rendering that Lexical JSON directly into native mobile components.

Now... I know already... The "standard" escape hatch for this problem is usually a WebView. It’s a brilliant practical solution because it's built-in and simple. It’s so effective at bypassing the headache of native layout constraints that even the original developers of Lexical at Meta considered that building a custom native renderer might not be worth the massive effort. And honestly, from a pure "time-to-market" perspective, they aren't wrong.

But if I’m being candid, WebViews have always felt like a "hacking" dirty workaround to me. I’ve used them before to render Lexical content, and it just never "felt right." You lose that snappy, native tactile feel; the fonts often look slightly off and you sometimes need to fightback against the renderer because some styles, scrolling effect, or interactions doesn't fit right. Since I had the opportunity and the time to actually dig into a proper React Native implementation, I figured, why not do it?

To bridge this, I've been working with an extracted fork of the @payloadcms/richtext-lexical package. It is specifically designed to give Lexical content a native home in the React Native world.

Inspiration

If you’ve ever peeked at the source code for the official Payload React renderer, the architecture of this React Native version will feel familiar. They share a significant amount of similarities because the RN implementation is based on the React's. This structural parity was intentional; I wanted developers who are comfortable with Payload on the web to feel immediately at home when moving to mobile, keeping the learning curve as flat as possible. And I wanted it to be a drop-in replacement!

Especially its API footprint. The main entry point is still a RichText component that consumes the same serialized JSON data you’d fetch for a web frontend. Just like its web counterpart, the RN renderer is built to be extensible. If you have a custom "Call to Action" block defined in your CMS, you can register a custom converter for it in the mobile renderer just as easily as you would on the web. This shared logic ensures that the "data-to-UI" pipeline remains consistent across your entire stack, even if the final pixels look different.

Implementation

To add React Native rendering support to the Lexical package, I mirrored the structure of the existing React renderer but swapping web elements for RN primitives. Since this is essentially a translation layer between structured JSON and native mobile components, the "magic" happens in how we map web-centric logic into the rigid, component-based world of React Native.

At the heart of the implementation is the existing recursive traversal engine. Lexical produces a nested tree where every node has a type. On the web, you might just map a paragraph type to a <p> or <div> tag which are "imported" implicitly. In React Native, we need to do things "explicitly".

The renderer takes the root node and iterates through its children. For each node, it looks up a Converter. This converter is a function that receives the node's data and returns a React Native element. If that node has children (like a list containing list items), the converter calls the transformation function again, effectively "drilling down" until it hits leaf nodes like text.

Primitives

Unlike the web, where you can assume a <div> exists, React Native requires you to import components like View and Text explicitly. More importantly, most professional apps use a custom design system. Which is why I introduced Primitives.

The resolvePrimitive utility solves the problem of hardcoded components. Instead of the renderer being strictly tied to standard React Native View or Text imports, it uses an internal registry. This registry functions as a mapping of abstract keys like Text, View, Image, Link, and Quote to actual components. This injection occurs when you use the RichText component, allowing you to pass a primitives prop to swap out the defaults for your own design system. The ultimate benefit is that the renderer stays perfectly in sync with your app’s specific typography and spacing without requiring you to rewrite the actual conversion logic for every paragraph or header.

Installation

You can install @lizardglobal/payload-richtext-lexical-react-native via your package manager of choice:

typescript
1# npm
2npm install @lizardglobal/payload-richtext-lexical-react-native
3
4# pnpm
5pnpm add @lizardglobal/payload-richtext-lexical-react-native
6
7# yarn
8yarn add @lizardglobal/payload-richtext-lexical-react-native


Then, import the RichText component and pass your serialized content:

typescript
1import { RichText } from "@lizardglobal/payload-richtext-lexical/react-native";
2import { Text } from "@/components/reusable/text";
3
4<RichText data={serializedLexicalContent} primitives={{ Text }} />

The expected usage flow was meant to achieve parity with the web experience:

  1. Fetch serialized Lexical data from Payload.
  2. Render it with RichText from the mobile export.
  3. Provide an onExternalLinkPress handler (crucial since RN handles URLs differently on iOS vs. Android).
  4. Override primitives or converters if you have custom node types.

Edge cases

  • Unknown Nodes: Currently, if the content includes a node type with no registered converter, it renders nothing. It’s a silent fail, which isn't ideal, but it prevents the app from crashing.
  • Internal Links: Routing in React Native is highly specific to the library you use (Expo Router, React Navigation, etc.). I’ve left internal link resolution to the developer. You'll need to override the Link converter to handle your app's specific navigation logic.
  • Performance: Recursive conversion of large JSON trees can be heavy on the JS thread. I’m looking into profiling this to ensure smooth scrolling on lower-end devices.
  • Tables & Images: The current table implementation is "barebones," using simple View blocks. Similarly, images don't have the automatic "srcset" magic of the web. You’ll need to leverage Payload's metadata to handle sizing and caching manually.

Since this is my first real contribution to the Payload ecosystem, and open source as a whole. I’m particularly eager to hear from those who are already wrestling with cross-platform content. I’ve leaned heavily on the existing React renderer for inspiration, but translating web-centric logic into the rigid world of React Native is a bit of a balancing act. If you find that certain implementation decisions don't quite align with your own mobile workflow, please reach out.