How to Setup React Native Web in a Remix project

Easy way to use React Native components in your Remix projects and reuse code
February 6, 2022 ยท 6 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";
import { hydrate } from "react-dom";
import { AppRegistry } from "react-native";
import { ReactNativeStylesContext } from "./rn-styles";

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

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

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

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

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";
import type { EntryContext } from "remix";
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

Other publications