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. Spin up your own project repo OR clone and modify the "React Native Example Repo" (see "React Native Example Repo" section)
- Note that our examples assume use of the Expo framework because it offers useful tools for local development, creating builds for web/native/simulators, and a router package. If you'd like to use the same React Native + Expo starting point, follow this documentation to set up a new Expo project
- A Sanity project with a hosted or self-hosted Sanity studio. (If you use the "React Native Example Repo", see the "Dependencies" subsection of the "React Native Example Repo" section).
Note that your application should use the Platform.OS
functionality of the react-native
library to ignore any web-specific code used for Visual Editing in the context of native builds, since Visual Editing is only used in the context of Sanity Studio (which runs in a browser). The examples below (and the "React Native Example Repo") take this into account using an isWeb
util and a Sanity-provided util called isMaybePresentation.
Hosting Services and the Content Security Policy Header
Whatever hosting service you use to serve the web build of your Expo app must allow you to set a custom Content Security Policy header (details below). For this reason, Expo's own hosting service will not work, as the Content Security Policy header is not configurable on that platform (as of 2025-10-06).
A valid example Content Security Policy header is: "frame-ancestors 'self' http://localhost:8081 https://www.sanity.io https://visual-editor-react-native.vercel.app https://rn-visual-editor.sanity.studio"
In this example, the URLs (in order) are for:
- A development environment for the React Native app.
- 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).
- The deployed React Native app.
- The deployed Sanity Studio.
React Native Example Repo
If you prefer to start from a working example 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. Its web app builds can be deployed to Vercel and its native builds can be created via Expo build servers. This application includes:
- The required front-end code snippets for Visual Editing (see the "Implementation in a New or Existing React Native/Expo App" section below for implementation details).
- 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.
Dependencies
As mentioned in the repo's Readme, the repo assumes you have created your Sanity Studio via the following steps (to set up content types and test data that match the front end application):
- 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".
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
To setup Presentation tool in your Sanity Studio, import the tool from sanity/presentation
, add it to your plugins
array, and configure previewUrl
and allowedOrigins
as shown below.
We recommend using environment variables loaded via a .env
file to support development and production environments. In the code block below, 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).
export default defineConfig({
...rest of studio config,
plugins: [
...other plugins,
presentationTool({
allowOrigins: [
process.env.SANITY_STUDIO_REACT_NATIVE_APP_HOST,
],
previewUrl: {
initial: process.env.SANITY_STUDIO_REACT_NATIVE_APP_HOST
}
})
],
})
Note that if you are using the "React Native Example Repo", you should add resolve: locationResolver
to the presentationTool config (in the main config object) where locationResolver
is:
export const locationResolver = {locations: {
// Resolve locations using values from the matched document
movie: defineLocations({
select: {
title: 'title',
slug: 'slug.current',
},
resolve: (doc) => ({
locations: [
{
title: 'Movies Directory',
href: '/movies',
},
{
title: `Movie Page: ${doc?.title}`,
href: `/movie/${doc?.slug}`,
},
],
}),
}),
person: defineLocations({
select: {
name: 'name',
slug: 'slug.current',
},
resolve: (doc) => ({
locations: [
{
title: 'People Directory',
href: '/people',
},
{
title: `Person Page: ${doc?.name}`,
href: `/person/${doc?.slug}`,
},
],
}),
}),
}}
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. people are used both in the People Directory at /people
and in their individual movie page at /movie/:movide_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.
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
wherePORT
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.
- For local development origins, enter
- Select Save.
What about "Allow credentials"?
If the calling origin needs to be able to send Sanity tokens, select ”Allow credentials.” In most cases, this is not necessary for front-end applications, but is necessary for Sanity Studios. However, if (for example) your front end application hits a back-end API to trade user login credentials for a Sanity token (to query a private dataset from the front end), you will need to Allow Credentials. Reverse proxy servers which perform the queries themselves on behalf of the front end do NOT need any CORS configuration as they do not run in the browser.
See the CORS documentation for more info.
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.
You might not need the rest of this guide
The rest of this guide shows the code snippets for enabling Visual Edting in your application. These snippets are already implemented in the React Native Example Repo (see above), so they are only required for implementations where you are adding Visual Editing to an existing project OR starting from scratch.
If you are using that example repo, stop here, you're all done (or read onward to learn more about how the code works).
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 environment variables are below (the "EXPO_PUBLIC" prefix can be abandoned if not using Expo and/or replaced with any required prefix for your build service):
# .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:
export const SANITY_PROJECT_ID: string =
process.env.EXPO_PUBLIC_SANITY_PROJECT_ID || '';
export const SANITY_DATASET: string =
process.env.EXPO_PUBLIC_SANITY_DATASET || '';
export const SANITY_STUDIO_URL:string =
process.env.EXPO_PUBLIC_SANITY_STUDIO_URL || '';
Application Setup
Add helper utilities
The isWeb
utility determines if you are in the native or web context. createDataAttributeWebOnly
is a helper function that helps create "data-sanity" attributes, which are used to enable click-to-edit overlays in Visual Editing/Presentation mode. These custom 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. Many field types are automatically enabled via the "stega" configuration in the Sanity client, but some fields (e.g. images) require the data attribute to show an overlay. Overlays can also be visually customized to have a different look and feel, display more contextual information, and so on. Learn more about data attributes and custom overlays in the docs.
import { isMaybePresentation } from "@sanity/presentation-comlink";
import { createDataAttribute, CreateDataAttributeProps } from
'@sanity/visual-editing';
import { Platform } from "react-native";
export const isWeb = Platform.OS === 'web'
export const createDataAttributeWebOnly =
(attr: CreateDataAttributeProps) => {
if (isWeb && isMaybePresentation()) {
return createDataAttribute(attr)
}
return ''
}
Add data attributes for React Native
Data Attributes in React Native
React Native components cannot directly use data attributes like data-sanity
as props.
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.
function SomeComponent({ _id, _type }) {
const attr=createDataAttributeWebOnly({id: _id, type: _type, path: 'poster'})
return (<WhateverParent>
<Image
// @ts-expect-error The react-native-web TS types haven't been
// updated to support dataSet.
dataSet={{ sanity: attr.toString() }}
source={{ uri: urlFor(poster).url() }}
style={styles.image}
/>
</WhateverParent>)
}
Repeat this for any components that need the data-sanity
attributes.
Configure the Sanity Client
Create a Sanity client instance to handle fetching data from Content Lake.
As mentioned in the "Add utils" section above, the stega
option enables automatic click-to-edit overlays for basic data types when preview mode is enabled. You can read more about how stega works in the docs.
import { SANITY_DATASET, SANITY_PROJECT_ID, SANITY_STUDIO_URL }
from "@/constants";
import { isWeb } from "@/utils/preview";
import { createClient } from "@sanity/client";
export const client = createClient({
projectId: SANITY_PROJECT_ID,
dataset: SANITY_DATASET,
useCdn: true,
apiVersion: '2025-05-30',
stega: {
enabled: !!isWeb,
studioUrl: SANITY_STUDIO_URL
}
})
Add the Sanity React Loader hooks for queries and live mode
You will fetch data in your pages/components (see below) with useQuery
. This handles querying when you are not in Presentation/Visual Editing mode. For example, in the React Native mobile or web context outside the Sanity Studio.
When you enter Presentation mode in the Sanity Studio, the useLiveMode
hook takes over data hydration responsibilities.
// sanity.ts
import { createQueryStore } from '@sanity/react-loader';
import { client } from '../sanity/client';
const { useLiveMode, useQuery } = createQueryStore({ client, ssr:false })
export { useLiveMode, useQuery };
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:
- when viewing content in the Presentation tool for whatever"Perspective" is currently chosen in that Presentation UI (Draft Perspective, Published Perspective, etc).
- when viewing published in your user-facing production application.
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.
Private datasets
Private Datasets
The useQuery
hook from @sanity/react-loader
does not currently support a "token" parameter, so it does not currently support querying private data from the user-facing front end application outside of Presentation mode. Inside of Presentation mode, useLiveMode
takes care of rendering whatever data matches the chosen Perspective in the Presentation UI.
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>
}
Add a SanityVisualEditing component
The enableVisualEditing
function handles rendering overlays, enabling click to edit, and re-rendering elements in your application when content changes. Note that this is where we call useLiveMode
. A helper component for that function is below/
If you do not use Expo Router
In the code example below, we flush changes that are made in the Presentation tool's URL bar from the Presentation tool to the Expo Router using the update
handler inside of the history
prop. This allows the React Native app to navigate from view to view in response to those URL bar changes.
If you do not use Expo Router, replace the calls to router.push
, router.back
, and router.replace
with the corresponding function calls from your chosen navigation methodology (e.g. the methods exposed as part of React Navigation's navigation
object).
import { useLiveMode } from '@/hooks/useQueryStore';
import { isWeb } from '@/utils/preview';
import { isMaybePresentation } from '@sanity/presentation-comlink';
import { enableVisualEditing } from '@sanity/visual-editing';
import { usePathname, useRouter } from 'expo-router';
import { useEffect } from 'react';
import { client } from '../sanity/client';
// This component only has an effect in presentation mode on the web -- it provides clickable overlays of content that enable Visual Editing in the studio.
export default function SanityVisualEditing() {
const pathname = usePathname()
const router = useRouter()
useEffect(() => {
const disable = isWeb && isMaybePresentation() ? enableVisualEditing({
history: {
// Handle user changes to the expo router pathname (e.g. clicking a link in the app) by updating the URL bar
subscribe: (navigate) => {
console.log('NAVIGATION EVENT:', {navigate, pathname})
// We navigate to Expo Router's current pathname.
navigate({
type: 'push',
url: pathname,
})
// Return cleanup function
return () => {}
},
// Handle user changes to the contents of the Presentation modeURL bar by calling expo router functions
update: (u: any) => {
console.log('URL UPDATE:', u)
switch (u.type) {
case 'push':
return router.push(u.url)
case 'pop':
return router.back()
case 'replace':
return router.replace(u.url)
default:
throw new Error(`Unknown update type: ${u.type}`)
}
}
},
zIndex: 1000,
// Handle the refresh button in the Presentation mode URL bar. (show spinner for 1 sec, refresh doesn't do anything for client-side apps)
refresh: (payload) => {
console.log('REFRESH EVENT: ', payload)
const { source } = payload
if(source === 'manual') {
return new Promise(resolve => setTimeout(() => resolve(undefined), 1_000))
} else {
return false
}
},
}) : () => null
return () => disable()
}, [pathname])
if(isWeb && isMaybePresentation()) {
useLiveMode({client })
}
return null
}
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
If you don't use Expo Router and its Stack
component, replace the Stack functionality in the code example below with your preferred view rendered (e.g. the Stack
component from React Navigation's createNativeStackNavigator
function)
import { Stack } from 'expo-router';
// Example -- could be whatever context provider or parent component you want at the root.
import SomeParent from "@/components/SomeParent"
export default function RootLayout() {
return (
<SomeParent>
<Stack>
<Stack.Screen name="(pages)" options={{ headerShown: false }} />
</Stack>
<SanityVisualEditing />
</SomeParent>
);
}
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:
import { useQuery } from '@/hooks/useQueryStore';
import { useLocalSearchParams } from 'expo-router';
export default function MovieScreen() {
const { page_slug } = useLocalSearchParams()
const query = groq`*[_type == "some_type" && slug.current == $page_slug][0]{...}`
const { data } = useQuery(query, { page_slug })
// ...unpack data and use to render page/components
}
Components like this are where you will render the React Native components with the dataSet
prop and its sanity
child for clickable overlays (see the "Add data attributes for React Native" section above).
Try out visual editing
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.
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.
Was this page helpful?