How to Setup React Native Web in a Remix project

Easy way to use React Native components in your Remix projects and reuse code#react#remix#react-native
February 7 · 3 min read

I assume you're here because you want to know how to set up React Native Web in your Remix project. Well, you're lucky, I had to do this a few days ago, and I haven't run into trouble with it yet, so here's a tutorial about it:

Let's get started!

The result of this tutorial is also available as a GitHub repository that you can just clone to get started with your project: https://github.com/HorusGoul/remix-react-native-web-starter

1. Installing the react-native-web package

The first thing you have to do is install the react-native-web package. However, since we can't customize our build process because Remix doesn't allow it yet, we'll need to use a package manager that will enable you to install a package with an alias. In this case, I decided to use pnpm.

$ pnpm add react-native@npm:react-native-web

Then, the types if you're using TypeScript. Note that not all types will be correct for a React Native Web project, but that's out of the scope of this tutorial.

$ pnpm add --save-dev @types/react-native

2. React Native Web Styles

React Native Web has its own way of handling styles, and for SSR and hydration, they give you a stylesheet element that you have to place in the <head> of your page.

We'll pass that stylesheet element to our Root using the Context API. Let's do that by creating a rn-styles.tsx file inside the app folder. Here's the content for that file:

import { createContext, useContext } from "react";

export const ReactNativeStylesContext =
  createContext<React.ReactElement<unknown> | null>(null);

export function useReactNativeStyles() {
  return useContext(ReactNativeStylesContext) ?? ReplaceWithStylesSSRTag;
}

export const ReplaceWithStylesSSRTag = <meta name="REPLACE_WITH_STYLES" />;

Now, let's move to the app/root.tsx file. We'll now use the useReactNativeStyles() hook and put the stylesElement inside the <head>. Also, we'll wrap the <Outlet /> component with a View with a few properties to match the React Native Web behavior.

...

import { useReactNativeStyles } from "./rn-styles";
import { View, StyleSheet } from "react-native";

...

export default function App() {
  const stylesElement = useReactNativeStyles();

  return (
    <html lang="en">
      <head>
        ...

        {stylesElement}
      </head>
      <body>
        ...

        <View pointerEvents="box-none" style={styles.appContainer}>
          <Outlet />
        </View>

        ...
      </body>
    </html>
  );
}

const styles = StyleSheet.create({
  appContainer: {
    flex: 1,
  },
});

We also need to add a global stylesheet to Remix that contains the following.

html, body {
  height: 100%;
}

body {
  display: flex;
}

Take a look at the Remix Docs about Stylesheets if you don't know how to add CSS in a Remix project.

3. SSR and Hydration

Assuming you haven't modified the app/entry.client.tsx file, just replace its contents with the following:

import { RemixBrowser } from "@remix-run/react"";
import { hydrateRoot } from "react-dom/client";
import { AppRegistry } from "react-native";
import { ReactNativeStylesContext } from "./rn-styles";

const App = () => <RemixBrowser />;

AppRegistry.registerComponent("App", () => App);

// @ts-ignore
const { getStyleElement } = AppRegistry.getApplication("App");

hydrateRoot(
  document,
  <ReactNativeStylesContext.Provider value={getStyleElement()}>
    <App />
  </ReactNativeStylesContext.Provider>
);

What's going on with that code? We're using React Native Web AppRegistry to get the initial styles for the app and avoid hydration from failing.

Now, let's move to the app/entry.server.tsx file, where we'll do a similar thing. Take a look at the code:

import { renderToString, renderToStaticMarkup } from "react-dom/server";
import { RemixServer } from "@remix-run/react"";
import type { EntryContext } from "@remix-run/node";
import { AppRegistry } from "react-native";
import { ReplaceWithStylesSSRTag } from "./rn-styles";

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  const App = () => <RemixServer context={remixContext} url={request.url} />;

  AppRegistry.registerComponent("App", () => App);

  let markup = renderToString(<App />);

  // @ts-ignore
  const { getStyleElement } = AppRegistry.getApplication("App", {});
  const stylesMarkup = renderToStaticMarkup(getStyleElement());

  markup = markup.replace(
    renderToStaticMarkup(ReplaceWithStylesSSRTag),
    stylesMarkup
  );

  responseHeaders.set("Content-Type", "text/html");

  return new Response("<!DOCTYPE html>" + markup, {
    status: responseStatusCode,
    headers: responseHeaders,
  });
}

This time, we're not passing the app styles using the ReactNativeStylesContext, but injecting them inside the markup by replacing a custom meta tag.

4. Using React Native Web components

One last thing you could do if you're doing this in a new project is to go ahead and replace your app/routes/index.tsx with the following:

import { Text, View } from "react-native";

export default function Index() {
  return (
    <View>
      <Text>Hello, world!</Text>
    </View>
  );
}

Then do pnpm dev and open your project in the browser to see your first Remix route rendered with React Native Web components!


I hope you liked this article! Don't forget to follow me on Twitter if you want to know when I publish something new about web development: @HorusGoul


Share article

Where to find me

Made with by me Source code available on GitHubFollow me on Twitter if you want to know more about my future articles, projects, or whatever I come up with!