The project is a starting point for creating React Native apps (using Expo) which correctly load and are editable within Sanity's Visual Editor (via the Presentation plugin in Sanity studio).
For more information on the implementation in this rep, visit Sanity's "Visual Editing with React Native" docs.
IMPORTANT: For the easiest starting point, this repo assumes that you have set up a Sanity project/studio and used the "Movies" starter template using the bootstrapping steps below.
Without the Sanity studio set up, the runtime of the React Native app itself shouldn't crash, but you won't see anything load on the test "movie" and "people" screens (feel free to remove them and update the nav if they are not needed, and in that case, you can set up a Sanity project/studio however you prefer and set the env file accordingly to point to that project -- see below).
BOOTSTRAP STEPS FOR YOUR SANITY STUDIO
Note that the following steps are intended to be run in whatever repo you use for your Sanity Studio project -- they are NOT bootstrap steps for this visual-editor-react-native
repo. For development in this repo, see "Development" below.
- Run
sanity init
in some repo (preferably a separate repo for simplicity and organization, since Sanity Studio is built on regular React, not React Native). - When that
sanity init
script asks you to choose a project template, chooseMovie project (schema + sample data)
. - When the init script asks
Add a sampling of sci-fi movies to your dataset on the hosted backend?
, you choose yes. - Make sure that in that project's "API" tab on https://manage.sanity.io, you've added the following hosts to the allowed CORS origins (WITH credentials allowed if your front end queries will pass a Sanity token, see Token Management below):
http://localhost:8081
(or whatever host/port you run the React Native app on)http://localhost:3333
(or whatever host/port you run your Sanity Studio on)
- Added the
sanity/presentation
npm library to that Sanity Studio repo and in that repo add the following config to the "plugins" section of your sanity.config.ts/js:
presentationTool({
resolve: locationResolver,
previewUrl: {
origin: 'http://localhost:8081',
previewMode: {
enable: '/preview-mode/enable',
disable: '/preview-mode/disable',
},
},
})
where the locationResolver is defined as:
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}`,
},
],
}),
}),
}}
NOTE: pnpm is recommended, development using other package managers has not been rigorously tested.
-
Create a Vercel project for your Expo web app (or a project on a similar hosting service -- MAKE SURE to choose one where you can set custom Content Security Policy headers, see "Deployment" section below for a valid example header).
-
Create an Expo project for the Expo web builds and add its project ID to
app.json
(replace the existing project ID, not shown for cleanliness):
"eas": {
"projectId": "" <--- put your Expo project ID here!
}
-
If using Vercel, link the repo to your project via
npx vercel link
. -
In the Vercel project's Environment Variables UI (or wherever you manage your env vars), add the following vars for each env you want to support (add at least Development and Production):
EXPO_PUBLIC_SANITY_PROJECT_ID=YOUR PROJECT ID EXPO_PUBLIC_SANITY_DATASET=THE DATASET FOR THE ENV EXPO_PUBLIC_SANITY_STUDIO_URL=THE URL OF YOUR SANITY STUDIO FOR THE ENV
Only for Vercel: add
ENABLE_EXPERIMENTAL_COREPACK=1
, since corepack is enabled in thevercel.json
build step.When using Vercel:
For local development or local native builds, run "npx vercel env pull" to generate a .env.local file that Expo can use. For the deployed Expo web app build, Vercel should pick up the Production env you set up in the Vercel API.
-
Add the same enviroment variables in your Expo project's Environment Variables UI
-
Install dependencies
pnpm install
-
Run the expo project (clears the metro cache)
pnpm start
Note: If you see an error warning in Cursor/VSCode in tsconfig.json about
expo/tsconfig.base
not existing, and you have already run the start command for the repo, sometimes you need to restart Cursor/VSCode (the IDE seems to have issues picking up the fact that expo starting up for the first time creates a .expo folder and clears that type error).
The Expo app will now be running and can be opened in the browser (project default host is http://localhost:8081
) or in the iOS simulator (see the console in the terminal window that is running Expo for instructions on the different options/features enabled by Expo Go).
The main goal of this repo is to be able to open the Expo app INSIDE of Sanity Studio, so once you have started the expo app
So once you have started up the expo app:
- Start the Sanity Studio steps above (start it up from its repo/directory -- the Studio code is not present in this codebase).
- Once that studio is running locally, visit the locally running studio (Sanity default host is
http://localhost:3333
) - Click the "Presentation" tab to view the React Native app inside your Sanity Studio.
You can start developing by editing the files inside the app directory. This project uses file-based routing.
You will almost certainly want to edit the home screen contents, remove the movies/people pages/components, and add your own pages/components, but you'll need to understand how to use several key features before modifying/removing any code. These features are:
useQuery
: This hook is required in order to load data from Sanity (which is automatically kept up to date when in Presentation mode -- under the hood theuseLiveMode
hook takes over, but your components should just needuseQuery
becauseuseLiveMode
is already configured at the app root inside theSanityVisualEditing
component).dataSet
: This prop for React Native scalar components (Image
,Text
,View
, etc) is optional, used to enable Visual Editing features for non-text elements. Text elements (strings, rich text, etc) already have click-to-edit enabled by default (because of thestega
option on our Sanity client). If you want to enable overlays for non-text elements, drag-and-drop for sortable arrays of content blocks, etc, you need to pass{ sanity: attr }
where theattr
is a sanity data attribute created usingcreateDataAttributeWebOnly
from /utils/preview.
To understand how to use both of these features in your own pages/components:
- Check out the /app/(tabs)/movies.tsx file (uses
useQuery
for data anddataSet
to enable click-to-edit poster images) and the /app/movie/[movie_slug].tsx file (usesuseQuery
for data fetching anddataSet
to enable drag-and-drop cast members list).** - read Sanity's "Visual Editing with React Native" docs.
NOTE the useQuery
hook from @sanity/react-loader
does not currently support a "token" parameter, so it does not currently support querying a private dataset when you are NOT in Presentation mode in the Sanity Studio.
When you ARE in Presentation mode in the Sanity Studio, the useLiveMode
hook takes over from useQuery
for data fetching.
That useLiveMode hook fetches data using a session cookie (set by the Presentation Plugin) to make queries that can include private data and draft content (for live editing updates).
The useLiveMode hook respects the user's role when determining which data/content types that user can access in Presentation mode (including Custom Roles).
TO QUERY PRIVATE DATA OUTSIDE PRESENTATION MODE --- create a private querying hook (call it usePrivateQuery
or useSanityQuery
or whatever you prefer) that allows you to perform token-authorized queries. However, NEVER add that token to the client side bundle/environment, since IT IS AN API KEY. Some example approaches for how to perform secure queries to private datasets in your private querying hook:
- 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 that private querying hook, decide conditionally at runtime whether to call the Sanity React Loader's useQuery
or your own usePrivateQuery
(or whatever you've named it), depending on whether you are in Presentation mode in the weg context. Determining whether you are in Presentation mode can be done with a helper from @sanity/presentation-comlink
called isMaybePresentation
and the web context can be checked using the isWeb
util from this repo.
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"
const { isWeb } = import '@/utils/preview";
<!-- 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 } = isWeb && isMaybePresentation() ? useQuery(query) : usePrivateQuery()
return <div>...contents</div>
}
In Presentation Mode
When you ARE in Presentation mode, useLiveMode
as implemented above will use the Sanity Live Content API to 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, live in real time, but you can also choose "Published", custom perspectives if they are enabled for your studio, etc.
In User-Facing Application
When you are NOT in Presentation mode, to use the Live Content API, you must implement a connection mechanism for it in your project. A package is WIP 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.
Learn more about the Live Content API here.
I've noticed that very occasionally on a clean install, pnpm install
does not seem to install all of expo's dependencies (sometimes the error is shown at install and sometimes at runtime) -- when I run into this, I generally just remove node_modules and pnpm-lock.yaml, clear the pnpm cache (pnpm cache delete
), and re-run pnpm install
. NOTE that by removing the lockfile you may advance your dependency versions, so be prepared for those changes (or roll back to a previous version of the lockfile and remove/reinstall the node_modules).
This repo uses the Expo build servers to generate builds of your React Native app (web, simulator, device) and uses Vercel to host the web build (for loading in Presentation). You are not required to use Vercel but whatever service you choose MUST allow you to customize the Content Security Policy header used by the web app. VERIFY that the hosting service allows this before choosing a provider.
A valid example 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.io's Dashboard (a centralized "content operating system" web application where deployed Studios and Sanity SDK applications are "installed" in a single organization-level view. Learn more about Dashboard.)
- your deployed React Native app
- the individual deployed Sanity Studio.
Make sure to change the projectId in app.json to your own project's ID.
Make sure you have an Expo project in the Expo dashboard with your environment variables defined (see above)
Follow Expo's guides building for iOS simulator, iOS, Android, etc and chosen environment (development, preview, production, etc), depending on your use case. (This project was built successfully as a preview build for iOS simulator, so it should work for at least that use case).
In this codebase, I've set the projet up to deploy the web build of the Expo app to Vercel hosting with:
pnpm deploy:web
I've configured the web app's vercel.json to add a correct CSP header that allows my own sanity studio URL to load this web app in an iframe (see vercel.json). Update the CSP header rewrite in vercel.json to use your own studio URL or refactor the codebase to use a different hosting service (as long as it can set the Content Security Policy header, see the warning above).
Add all deployment and local development URLs for this project to the Sanity project's CORS origins. Any host that wants to query your data in Sanity has to be configured in those project CORS settings (set Allow Credentials to true). Use the Sanity Manage console to update CORS settings.
Several standard modules from Node that are part of the @sanity library but are not in the React Native runtime are shimmed using metro.config.js. Run the expo start command above with the --clear
flag to clear the metro cache if you make additions/modifications to those shims for your own use case.