Visual Editing

Visual Editing with React Native

Get started with Sanity Visual Editing in a new or existing React Native application using the Sanity React Loader.

Following this guide will enable you to implement:

  • Live preview: See draft content updates rendered in the embedded front end in real-time.
  • Click-to-edit: Interactive overlays for the embedded front end application that help content creators find and edit the right fields.
  • Page building: Advanced capabilities for adding, moving, and removing content sections, directly from your embedded front end.
  • Preview sharing: A way for content creators to share a preview of draft content with others.
  • Locations: Shortcuts to open Presentation (the embedded front end view) for a document directly from where the content is defined the Structure tool.

Your deployed web build of the application will be loaded loaded into your deployed Sanity Studio via the Presentation plugin (and you can do the same for locally running versions of your app and your Studio, which streamlines development and debugging).

Prerequisites

  • A React Native application. Note that though the Expo framework is not required, this guide and our starter repo use it because it offers useful tools for local development, creating builds for web/native/simulators, and a convenient router package. If you're building a new React Native application and you want to use a different framework, there will be some small differences (noted in this guide where possible). However, if you want to match/follow the examples exactly, you can either:
  • A Sanity project with a Sanity-hosted or self-hosted Sanity studio.
    • (If you use the "React Native Starter" repo, see the "Dependencies" subsection of the "React Native Starter" section for a Studio setup that matches that starter).

Note that because Presentation enables Visual Editing by loading your React Native application as a web build into the browser-based Sanity Studio, much of the code for enabling Visual Editing only runs when you're in the browser and in Presentation mode.

The examples below (and the "React Native Starter" repo) take this into account using an isWeb util and a Sanity-provided util called isMaybePresentation.

Content Security Policy

Hosting Services and the Content Security Policy Header

A valid example Content Security Policy header is:
"frame-ancestors 'self' http://localhost:8081 https://visual-editor-react-native.vercel.app https://rn-visual-editor.sanity.studio https://www.sanity.io"

In this example, the URLs (in order) are for:

  • The localhost/port combination where the web build server for the React Native app runs in local development.
  • The deployed web build of the React Native app.
  • The deployed Sanity Studio.
  • The Sanity Dashboard (the centralized "content operating system" web application where deployed Studios and Sanity SDK applications are "installed" in a single organization-level view. Learn more about the Dashboard).

React Native Starter Repo

If you prefer to start from a working example, remove the demo pages ("movies" and "people"), and add your own code, we have created a starting point repo for a React Native application (built on Expo) which is ready to be loaded into the Presentation tool out of the box. By default its native builds are created via the Expo build servers and its web builds are built on and hosted on Vercel (but you are free to refactor this to use any hosting service which allows you to set a custom "Content Security Policy" header, see above).

This starter application includes:

  • The required code snippets for Visual Editing
  • Example pages ("Movies", "People", etc) with layouts and routing already set up.
  • Utility components for building your own views.

The repo is fully open source - it is available on Github and has a comprehensive Readme for development and deployment.

The "Implementation in a New or Existing React Native App" section explains how the Presentation mode and its features are implemented (both in the starter repo and the examples in this guide), so reading the entire guide is helpful even if you use the starter repo!

Dependencies

Sanity Project/Studio

As mentioned in the repo's Readme, the starter repo works together with your own Sanity project/Sanity Studio.

If you want to see the Movies/People pages/components load actual data in the React Native app, your Studio will need to be created with the content types and test data from the "movies" Sanity Studio starter template.

We recommend this approach because it lets you play with a functioning version of the presentation features in your dev environment to better understand how those features work and how they correspond to the code snippets that enable them (useQuery, useLiveMode, dataSet, etc -- all discussed in this documentation, see below).

To create a Sanity project/studio which includes the "movies" starter's content types/data (you can modify/remove them later):

  • Run sanity init in some repo/folder (easiest/cleanest option is a separate repo, since Sanity Studio is built on vanilla React, not React Native).
  • When that init script asks you to choose a project template, choose "Movie project (schema + sample data)"
  • When the init script asks "Add a sampling of sci-fi movies to your dataset on the hosted backend?", choose "yes".

Otherwise, if you feel comfortable with all aspects of implementing the Visual Editing features in your own components and want to rip out the movies/people code immediately, you may initialize your Sanity project/studio whatever way you prefer.

See the CLI Init command docs for more info on project initialization and templates.

Presentation Plugin

You must add configuration for the Sanity Presentation Plugin to your Sanity Studio which matches the setup for your React Native application (see "Sanity Studio Setup" section below)

Sanity Studio Setup

Before working on setting up Visual Editing in the React Native app, we need to set up the Presentation plugin in your Sanity Studio.

First, include the library in your studio repo:

pnpm install sanity@latest
// or install with npm or yarn

You will now have access to the presentationTool plugin from sanity/presentation. As shown below, import it, add it to your plugins array, and configure previewUrl and allowedOrigins .

We recommend using environment variables loaded via a .env file to support development and production environments. In the code block above, SANITY_STUDIO_REACT_NATIVE_APP_HOST is the hostname of your front end React Native application that is going to be loaded into presentation mode (either running locally or deployed, depending on the env in question).

Note that if you are using the "React Native Starter Repo", you should add resolve: locationResolver to the presentationTool config (in the main config object) where locationResolver is:

This adds the functionality where "location" links are added to the top of each document in the Studio Structure view. Each of these links for a given document opens the Presentation tool and automatically loads the page where that document is used, directly in that embedded front end.

The locations are defined by the resolver function (e.g. each movie is used both in the Movies Directory at /movies and in its individual movie page at /movie/:movie_slug).

You can add additional location resolvers for your other content types (and/or remove the movies/people location resolvers if your are no longer using those content types).

See the "map content to front-end routes with locations resolver function" section in the Presentation Tool docs for examples and more info.

Locations Resolver in Deployed Projects

Add CORS Origins

Because our React Native application (and our Sanity Studio) will make client-side requests to the Sanity Studio across domains, their URLs must be added as valid CORS origins.

This can be done inside sanity.io/manage. Use the following steps for your React Native front end application and then repeat for your Sanity Studio.

  • Navigate to the API tab, then add select "Add CORS origin".
    • For local development origins, enter http://localhost:PORT where PORT is the port number that is running the application in question.
    • For deployed origins, add the full hostname of the deployed React Native application or Sanity Studio.
  • Select Save.

What about "Allow credentials"?

Only set up CORS origins for URLs where you control the code. Remember to perform the steps for each local development origin and each deployed origin for your front ends and your Sanity studio. One caveat is that deploying a Sanity-hosted studio will add the CORS config for that studio automatically. If you self-host the studio, you will need to add it yourself.

A Note About Using This Guide

For the React Native Starter Repo -- Code that's done vs to-do

Implementation in a New or Existing React Native App:

Install dependencies

Install the dependencies that will provide your application with data fetching and Visual Editing capabilities.

npm install @sanity/client @sanity/react-loader @sanity/visual-editing @sanity/presentation-comlink

Set environment variables

Create a .env.local in your application’s root directory to provide the configuration for connecting to your Sanity data.

For platform-native builds, if you are using Expo as your build service, you will also need to create the variables in the Expo Environment Variables console for your project. For other build services, follow the appropriate environment variable specification process outlined in the documentation of the service in question.

For the web build, if you are using a hosting service where env variables are created in a browser UI (e.g. Vercel), create the variables in that UI. Other hosting services may just expect a .env file in your codebase or you might set the vars in a CI/CD pipeline, etc—this step is specific to your hosting implementation.

You can use sanity.io/manage to find your project ID and dataset.

The URL of your Sanity Studio will depend on where it is hosted or embedded.

(The "EXPO_PUBLIC" prefix can be abandoned if not using Expo and/or replaced with any required prefix for your build service).

Define the following environment variables:

# .env.local or .env
EXPO_PUBLIC_SANITY_DATASET=Your dataset name
EXPO_PUBLIC_SANITY_PROJECT_ID=Your Sanity project ID
EXPO_PUBLIC_SANITY_STUDIO_URL=The URL of your Sanity Studio 
(running locally OR deployed, depending on env)

and import them into the runtime:

Add a Preview Utilities file

The isWeb utility determines if you are in the native or web context. We will add more utilities to this file later.

Configure the Sanity Client

Create a Sanity client instance to handle fetching data from Content Lake.

The stega option enables automatic click-to-edit overlays for all text content in Presentation mode. You can read more about how stega works in the docs.

Define the Sanity React Loader hooks for queries and live mode

You will fetch data in your pages/components (see below) with useQuery from the react-loader library. This handles querying your data when you are NOT in Presentation/Visual Editing mode, for example, in the actual React Native mobile app or in a web build loaded directly in a browser (rather than in Presentation mode in Sanity Studio).

When you enter Presentation mode in the Sanity Studio, the useLiveMode hook will take over the data hydration responsibilities.

We will see where/how to use these hooks in a moment, but for now we just create the query store and export the resulting hooks.

Define the SanityVisualEditing component

The imported enableVisualEditing function from handles rendering overlays, enabling click to edit, and re-rendering elements in your application when you make content changes. Via its history and refresh properties, it also connects the URL bar of the presentation tool with the internal routing of the React Native app, keeping the two in sync.

As shown below, configure the enableVisualEditing function and wrap it in a parent SanityVisualEditing component (note that here and only here is where we call useLiveMode):

If you do not use Expo Router

Render the SanityVisualEditing component

Add the SanityVisualEditing component to your root layout(s) outside of any "Stack" elements.

If you do not use Expo Router

Query data from Sanity and render a page

With the components included and loaders set up, you can call useQuery with each page's query and render the data. For example:

Click-to-Edit Overlays

Out of the Box

Enabling the stega option (as we did when we configured our Sanity client above) ensures that all text content will automatically have click-to-edit overlays.

These overlays allow you to click on any component that renders a piece of sanity content and automatically open that piece of content for editing in the visual editor's form sidebar.

Learn more about Overlays.

Data Attributes

To enable the same functionality for non-text fields (e.g. images), you can add a data-sanity attribute to the component that renders them.

To add a data-sanity attribute, we create it using a helper, createDataAttributeWebOnly, which is a React Native-specific helper that wraps for the Sanity createDataAttribute method and checks the runtime to ensure that we are in Presentation inside the Sanity Studio. Let's add this to our /utils/preview.ts file from earlier:

Data Attributes in React Native

Create the data attribute using the createDataAttributeWebOnly util set up above, and apply it to the React Native component using the dataSet prop. In this example, we are adding it to a react-native Image component, but it should work for any scalar React Native component that needs to carry the "data-sanity" on its underlying html tag. In the example below, we add it to the SomePage component we wrote earlier when learning to use useQuery:

import { useQuery } from "@/hooks/useQueryStore";
import { urlFor } from "@/utils/image_url";
import { createDataAttributeWebOnly } from "@/utils/preview";
import { useLocalSearchParams } from "expo-router";
import groq from "groq";
import { Image, Text, View } from "react-native";

export default function SomePage() {
  const { page_slug } = useLocalSearchParams();
  const query = groq`*[_type == "some_type" && slug.current == $page_slug]{...}`;
  const { data } = useQuery(query, { page_slug });

  return (
    <View>
      {data?.map((document: YourDocumentType) => {
        const { _id, _type, title, hero_image } = document;

        const attr = createDataAttributeWebOnly({
          id: _id,
          type: _type,
          path: "hero_image",
        });
        return (
          <View key={_id}>
            <Image
              // @ts-expect-error The react-native-web TS types haven't been
              // updated to support dataSet.
              dataSet={{ sanity: attr.toString() }}
              source={{ uri: urlFor(hero_image).url() }}
            />
            <Text>{title}</Text>
          </View>
        );
      })}
    </View>
  );
}

Repeat this for any of your components that need the data-sanity attributes.

Additional Presentation Mode Features

You can also:

These are out of the scope of this guide, but the examples in docs above are the same for React Native, with the exception of the fact that you must use the dataSet attribute in place of directly using data-sanity (as discussed above).

The React Native Starter repo does have an example of drag-and-drop in the [movie_slug].tsx component

A Note on Private datasets

Private Datasets

To query private data from user-facing applications, create a private querying hook (call it usePrivateQuery or useSanityQuery or similar) that allows you to perform token-authorized queries. However, never add that token to the client side bundle/environment, it is an API KEY. Some example approaches for how to perform such queries:

  • Build an API that has custom auth (for however you authenticate your users) and returns a token for the Sanity client to use in calls to client.fetch. This is the simplest approach but has the negative side effect that it exposes the token to the client side, so any logged in user can take that token and take any action for which the token is authorized—usually at a minimum this means making ANY query to your data, but can also even include writing data, updating settings, etc depending on the token.
  • Have a proxy API that has custom auth and can make queries on your behalf from the server, which never exposes the token to client side users. This allows you to either allow arbitrary queries if all authorized users should be able to make any query or even allows you to lock down which queries can be made by exposing API routes for individual queries.

Once you have defined a private querying hook, decide at runtime whether to call the Sanity React Loader's useQuery or your own usePrivateQuery/useSanityQuery/customQuery depending on whether you are in Presentation mode. Determining whether you are likely in/not in Presentation mode can be done with a helper from @sanity/presentation-comlink called isMaybePresentation.


So an example conditional usage of the correct hook for the platform/context might be like:

const { isMaybePresentation } = import "@sanity/presentation-comlink"
const usePrivateQuery = import "@/hooks/usePrivateQuery"
    
    <!-- In a real life example, put this "createQueryStore" call in its own module so that it is called ONLY once and imported into components where used -->

    const { useLiveMode, useQuery} = createQueryStore({ client, ssr:false })

    function SomeComponent {
        const { data } = isMaybePresentation() ? useQuery(query) : usePrivateQuery()

        return <div>...contents</div>
    }

A Note About the Live Content API

The Live Content API can be used to receive and render real time updates in your application without refreshing the page, both:

  • as used in Presentation mode in this guide -- immediately shows the latest data for whatever "Perspective" is currently chosen in that Presentation UI (Draft Perspective, Published Perspective, etc).
  • in your user-facing production application outside Presentation mode -- shows the latest published data (without needing to reload the page).

When you are in Presentation mode, useLiveMode will use a cookie set by the Presentation plugin to authenticate live updates from the Live Content API and show you the latest content for whatever "Perspective" you choose in the Presentation UI itself. The most common Perspective used is "Drafts", because that will show you all edits to documents, rendered in your embedded front end, live and in real time. This is how we enable instantaneous "Visual Editing". However, you can also choose the "Published" perspective to see a view of all published changes.

The useLiveMode hook respects the user's role when determining which data/content types that user can access in Presentation mode (including Custom Roles).

When you are not in Presentation mode, you must implement a connection mechanism for it in your project in order to use the Live Content API. A package is in-progress for an out-of-the-box Live Content API connector for vanilla React and React Native and will be added to this example when available.

For example/starting point implementations in the meantime, check the lcapi-examples Github Repo.

Visual Editing is Enabled!

With your front end application and Sanity Studio both running locally (or deployed) and configured using the appropriate environment variables and the code snippets from this guide, you should be able to open the Studio, click "Presentation", and see your front end application embedded as a click-to-edit view with automatic live content updates.

Once both the front end application and the Sanity Studio are deployed (and configured correctly in the Presentation plugin config in the Studio code), you will see the same functionality enabled for your deployed application.

If you don't see the page as expected, confirm the code snippets related to presentation are as expected, and take a look at the Visual Editing Troubleshooting guide (modifying data attributes to the dataSet format and making any other changes that are relevant to React Native or your application).

For additional support, join our Discord server, where Sanity support engineers are regularly active and assisting our community!

Was this page helpful?