Using React-Admin With React Native

Gildas Garcia
Gildas GarciaMay 22, 2024
#react#react-admin#react-native

React-admin is a web framework for building responsive applications using React. This means that react-admin applications can run on any device, as long as it has a web browser. However, what if you want to build a native application for mobile devices? Can you use react-admin for that?

In a previous article (Using React-Admin With Your Favorite UI Library), I explained that react-admin's headless architecture lets us use a different UI layer thanks to the ra-core package. In this article, I'll explain how to build a native mobile application with react-admin, powered by react-native. This application can also run in a web browser, thanks to the react-native-web package.

You can find the code for the demo application on the marmelab/react-admin-native-demo repository.

Setting Up The Project

For this article, I chose to use Expo, a popular tooling ecosystem for react-native. It provides a CLI to bootstrap a new project, run it in development mode on various devices (android, iOS, and web), and build it for production.

Let's get started by running the Expo command to create a new project:

bunx create-expo-app ra-native --template
I used Bun for this project. If you prefer yarn or npm, just replace bunx with npx or yarn dlx and bun with npm or yarn

I chose to use the Blank (TypeScript) template when asked.

To run a web version of the application, I need to install a few additional libraries:

bunx expo install react-native-web react-dom @expo/metro-runtime

I can now run the following command to start working:

bun start

In the terminal output, I can see that I must press w to open the browser version of my application and a to open the Android one. I suppose i would open the IOS version but I don't have any IOS devices.

Bootstrapping React-admin

Just like in the previous article (Using React-Admin With Your Favorite UI Library), I don't need the ra-ui-materialui package. Likewise, I won't install the react-admin package that glues ra-core and ra-ui-materialui together.

Instead, I can just install the ra-core package and build my own UI components for react-native. However, I still need at least a DataProvider and optionally an AuthProvider and I18nProvider. For this article, I chose to use the ra-data-fakerest DataProvider with some fake data from the data-generator-retail package.

bunx expo install ra-core@next ra-data-fakerest@next ra-i18n-polyglot@next ra-language-english@next data-generator-retail@next
Note that I install the latest beta version of the React admin packages. This is required as we fixed a few functions that relied on browser APIs.

To get started, I set up the DataProvider:

// in src/dataProvider.ts
import fakeRestDataProvider from "ra-data-fakerest";
import generateData from "data-generator-retail";

export const dataProvider = fakeRestDataProvider(generateData(), true);

I also need an i18nProvider:

// in src/i18nProvider.ts
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";

export const i18nProvider = polyglotI18nProvider(() => englishMessages, "en");

The ra-core package requires that I provide the layout. For now, I just need it to render its content provided through the children prop:

// in src/Layout.tsx
import type { ReactNode } from 'react';

export const Layout = ({ children }: { children: ReactNode }) => (
    <View>{children}</View>
);

I then set up a very basic dashboard component:

// in src/Dashboard.tsx
import { View, Text } from 'react-native';

export const Dashboard = () => (
    <View>
        <Text>Hello world!</Text>
    </View>
);

These components let me build the <Admin> component itself. It is the combination of two core components:

  • <CoreAdminContext>, responsible for giving all components access to our providers through React contexts
  • <CoreAdminUI>, responsible for setting up react-admin routes, wrapping them in the layout, etc.

I also have to provide a <Resource> to avoid the default setup screen.

// in src/Admin.tsx
import { CoreAdminContext, CoreAdminUI } from "ra-core";
import { dataProvider } from "./dataProvider";
import { i18nProvider } from "./i18nProvider";
import { Dashboard } from "./Dashboard";
import { Layout } from "./Layout";

export const Admin = () => (
  <CoreAdminContext
    dataProvider={dataProvider}
    i18nProvider={i18nProvider}
    dashboard={Dashboard}
  >
    <CoreAdminUI dashboard={Dashboard} layout={Layout} title="React Admin">
        <Resource name="products" />
    </CoreAdminUI>
  </CoreAdminContext>
);

Finally, I can modify the default App.tsx file at the project root to render my admin:

import { Admin } from './src/Admin';

export default function App() {
  return (<Admin />);
}

Opening the application in the browser greets us with our dashboard! However, if I open the application in either an emulator or a real device, I get this error message:

ERROR  ReferenceError: Property 'document' doesn't exist

This error is located at:
    in InternalRouter (created by AdminRouter)
    in BasenameContextProvider (created by AdminRouter)
    in AdminRouter (created by CoreAdminContext)
    in QueryClientProvider (created by CoreAdminContext)
    in PreferencesEditorContextProvider (created by CoreAdminContext)
    in StoreContextProvider (created by CoreAdminContext)
    in CoreAdminContext (at Admin.tsx:8)
    in Admin (at App.tsx:5)
    in App (at withDevTools.js:18)
    in withDevTools(App) (at renderApplication.js:57)
    in RCTView (at View.js:116)
    in View (at AppContainer.js:127)
    in RCTView (at View.js:116)
    in View (at AppContainer.js:155)
    in AppContainer (at renderApplication.js:50)
    in main(RootComponent) (at renderApplication.js:67), js engine: hermes

The reason for this error is that react-admin sets up a default React Router, which is meant to work in browsers only.

The good news is that this is the kind of issue for which react-native provides solutions.

Handling Platforms Differences

React-admin lets me define a custom router by wrapping my admin application with a router component. React Router actually provides a router for react-native, the <NativeRouter>. However, I only want to use it for mobile devices, not the web version.

To do that, I can provide two admin components and leverage React composition to avoid duplicating too much code.

First, I install the react-router-native package:

bunx expo install react-router-native

Then, I rename my src/Admin.tsx file to src/CommonAdmin.tsx.

Finally, I add two files:

  • src/Admin.native.tsx for mobile devices
  • src/Admin.web.tsx for browsers

In the src/Admin.native.tsx, I can simply wrap my Admin with the <NativeRouter> component:

import { NativeRouter } from "react-router-native";
import { Admin } from "./CommonAdmin";

export default function NativeAdmin() {
  return (
    <NativeRouter>
      <Admin />
    </NativeRouter>
  );
}

And in the src/Admin.web.tsx, I wrap it with the <BrowserRouter>:

import { BrowserRouter } from "react-router-dom";
import { Admin } from "./CommonAdmin";

export default function BrowserAdmin() {
  return (
    <BrowserRouter>
      <Admin />
    </BrowserRouter>
  );
}

Note that there's no src/Admin.tsx file anymore - only platform-specific versions. Also, I don't have to specify the .native or .web suffixes when importing the component. Indeed, react-native detects those suffixes and loads the one relevant to the platform currently executing the application. Should I need to differentiate Android and iOS, I could have scr/Admin.android.tsx and src/Admin.ios.tsx as well.

My dashboard is now displayed on both the browser and my Android device!

You might have to stop and restart the expo CLI to make it work.

Building Mobile UI Components

The main challenge was finding a UI library with enough components to support React-admin features. I chose react-native-paper, which seems maintained and provides enough components to get started.

Using this library and the react-admin core hooks, I was able to build all the UI components I needed, like the following Text Input:

import { InputProps, useInput } from "ra-core";
import { TextInput } from "react-native-paper";

export const RaTextInput = (props: InputProps) => {
  const { field } = useInput(props);

  return (
    <TextInput
      mode="outlined"
      label={props.label?.toString()}
      value={field.value}
      onChangeText={field.onChange}
    />
  );
};

The form state is still managed by react-admin (and, under the hood, by react-hook-form), so the code is pretty minimal.

I used this component on a Product edition page:


import { EditBase, useRecordContext } from "ra-core";
import { Form } from "../ui/Form";
import { RaTextInput } from "../ui/RaTextInput";
import { RaReferenceInput } from "../ui/RaReferenceInput";
import { View } from "react-native";

export const ProductEdit = () => {
  return (
    <EditBase mutationMode="pessimistic">
      <ProductView />
    </EditBase>
  );
};

const ProductView = () => {
  const record = useRecordContext();
  if (!record) {
    return null;
  }
  return (
    <View style={{ flex: 1, flexDirection: "column", gap: 16 }} >
      <Form>
        <RaTextInput source="reference" />
        <RaReferenceInput source="category_id" reference="categories" />
      </Form>
    </View>
  );
};

The styling is made easy by react-native's View component, which is a flexbox container by default. Again, I could reuse all the Edit page logic by using the EditBase component from ra-core.

Some components were a bit harder to build. The List pages are a good example. I had to build two custom ProductList components, one for mobile using a regular List, and the other for the web using a DataTable. I used the same technique as for the Admin component by providing two files with the .native and .web suffixes.

The Result

I could take you through the process of creating the whole application. However, it's not really different than what I already did in the previous Using React-Admin With Your Favorite UI Library article.

Instead, I'll show you the result. This is the application running on my Android device:

Here is the application running in the browser:

The application is fully responsive and works on both platforms. Most of the code is shared between the two versions, with only a few platform-specific components.

Conclusion

React-admin is a very flexible framework that can even be used to build native applications. The React-native ecosystem is vast and provides enough libraries to build a react-admin application that runs on mobile devices. However, to go beyond this proof-of-concept, you'll have to build some advanced components (like <Autocomplete>) yourself, and this can be time-consuming.

The React core team is currently working on an alternative solution for building universal React apps: It's called React Strict Dom (RSD). It's a new renderer for React that is designed to be compatible with both the web and mobile apps. It's still in its early stages but it's worth keeping an eye on it.

You can find the code for this application on the marmelab/react-admin-native-demo repository.

Did you like this article? Share it!