DEV Community

Cover image for Building a progressive web app in Remix with Remix PWA
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Building a progressive web app in Remix with Remix PWA

Written by Chimezie Innocent✏️

Progressive web apps (PWAs) are applications that look and behave like mobile apps, but are built using web technologies. While they leverage native mobile features, you access them through the browser like regular web applications, meaning you don’t have to download them via mobile app marketplaces.

PWAs have gained popularity in recent years thanks to their ability to combine web and mobile features. This ability provides users with a seamless experience, even when the application is offline, and even enables users to install the web app to their device like a native mobile app.

Remix is a robust, modern web framework that makes it easy to build performant, scalable apps. We can use Remix PWA — a lightweight, standalone framework — to combine the benefits of Remix and PWAs.

In this article, we will discuss Remix PWA, why it’s necessary, and how to integrate it into our Remix app with a demo. You can check out the complete code for our demo app on GitLab.

What is Remix?

Remix is a modern, full-stack web framework that extends the capabilities of the React library. It’s designed to help developers build highly responsive, blazing-fast web applications on the server. Some of its key features include, among many others:

As an SSR framework, Remix handles everything on the server before sending the information back to the user. This results in a faster and more interactive user experience. It’s worth noting that navigation and certain other interactions still occur on the client side, contributing to a smooth UX.

Why use Remix PWA?

Remix PWA is a lightweight PWA framework that integrates the features of a PWA into a Remix application. These features include caching, offline support, push notifications, and so on.

Combining PWA principles with the Remix web framework enables us to create web apps that offer a more reliable, performant user experience, making Remix PWA an important tool in your developer toolbox. Here are some of the features that Remix PWA provides:

  • Native experience: Remix PWA provides an app-like experience on the web. It allows users to add your web app to their home screens and run it as a standalone window without the browser UI, making it feel like a native application
  • Improved performance: PWAs are designed to provide a fast, smooth UI — and by extension, a great UX. By combining Remix’s server-side rendering with PWA features and capabilities, we can build highly performant web apps enhanced using key aspects of the native experience
  • Offline support: PWAs are designed to support offline or low-network access. Remix PWAs can cache resources for this purpose, ensuring that users can continue using the application even when they’re not connected to the internet
  • Push notifications: PWAs support push notifications, just like native mobile applications do. This feature allows you to send real-time updates and alerts to the user even when the app is not open in the browser
  • Cross-platform capability: Apps built with Remix PWA can function on different devices and platforms. Users can access the application either from the browser or from a mobile device
  • Extensibility: You can add your own caching strategies, native APIs, and more to Remix PWA. While it’s highly extensible, Remix PWA also provides you with multiple caching strategies, which are likely to cover most of the use cases you need
  • Modularity: Remix PWA isn’t bundled. Instead, it’s built from modular packages, making it modular by default for a better PWA experience

Now that we’ve seen the benefits of Remix and PWAs and the power of leveraging them together using the Remix PWA framework, let’s see how we can build our own Remix PWA.

Building a Remix PWA

Before we dive into the tutorial part of this article, ensure that you have the following installed:

  • Node.js
  • Your chosen JavaScript package manager: I will be using npm, but you can continue with Yarn if you already have that installed
  • Remix CLI

You should also already have basic knowledge of Remix, as this article will focus mainly on building the PWA. If you don’t, you can check out this guide to the Remix framework to familiarize yourself first. Otherwise, let’s jump right in.

Creating a Remix project

We will start by creating a new Remix project. Run the command below:

npx create-remix@latest
Enter fullscreen mode Exit fullscreen mode

Follow the prompts to configure your project. You can also use the command below to spin up your Remix project:

npx create-remix@latest remix-pwa-tutorial
Enter fullscreen mode Exit fullscreen mode

The command above creates a new Remix project with the name remix-pwa-tutorial and uses the basic template provided in the docs.

For Yarn users, note that when accepting the prompts, you should select No for the Install dependencies with npm? prompt. This ensures that you can install the dependencies and run the Remix project with Yarn instead of npm: Developer Terminal Showing Remix Setup Process Using Yarn Instead Of Npm

After completing the installation process, navigate into your project, run your yarn install command, and then you’re ready to go. Spin up the project with any of the commands below:

/* npm */
npm run dev

/* yarn */
yarn run dev
Enter fullscreen mode Exit fullscreen mode

I’m not using a template, so you can use this command to install your Remix app:

npx create-remix@latest
Enter fullscreen mode Exit fullscreen mode

Then, replace the contents of your root.tsx file with the following:

// root.tsx

import type { LinksFunction } from "@remix-run/node";
import {
  Form,
  Link,
  Links,
  Meta,
  Outlet,
  Scripts,
  useLoaderData,
  ScrollRestoration,
} from "@remix-run/react";
import { json } from '@remix-run/node';
import appStylesHref from './app.css';
import { getContacts } from './data';
import { useSWEffect, LiveReload } from '@remix-pwa/sw';

export const links: LinksFunction = () => [
  ...([{ rel: "stylesheet", href: appStylesHref }]),
];

export const loader = async () => {
  const contacts = await getContacts();
  return json({ contacts });
};

export default function App() {
  useSWEffect()
  const { contacts } = useLoaderData<typeof loader>();

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1"
        />
        <Meta />
        <link rel="manifest" href="/manifest[.]webmanifest" />
        <Links />
      </head>
      <body>
        <div id="sidebar">
          <h1>Remix Contacts</h1>
          <div>
            <Form
              id="search-form"
              role="search"
            >
              <input
                id="q"
                aria-label="Search contacts"
                placeholder="Search"
                type="search"
                name="q"
              />
              <div
                id="search-spinner"
                aria-hidden
                hidden={true}
              />
            </Form>
            <Form method="post">
              <button type="submit">New</button>
            </Form>
          </div>
          <nav>
            {contacts.length ? (
              <ul>
                {contacts.map((contact) => (
                  <li key={contact.id}>
                    <Link to={`contacts/${contact.id}`}>
                      {contact.first || contact.last ? (
                        <>
                          {contact.first} {contact.last}
                        </>
                      ) : (
                        <i>No Name</i>
                      )}
                      {contact.favorite ? <span></span> : null}
                    </Link>
                  </li>
                ))}
              </ul>
            ) : (
              <p>
                <i>No contacts</i>
              </p>
            )}
          </nav>
        </div>
        <div id="detail">
          <Outlet />
        </div>
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

The code above sets up a simple application for managing contact information.

Next, in your app directory, create a routes directory, then create a contacts.$contactId.tsx file inside the new directory. Copy the code below into this file:

// routes/contacts.$contactId.tsx

import type { LoaderFunctionArgs } from '@remix-run/node';
import { Form, useLoaderData } from '@remix-run/react';
import type { FunctionComponent } from 'react';
import type { ContactRecord } from '../data';
import { json } from '@remix-run/node';
import invariant from 'tiny-invariant';
import { getContact } from '../data';
export const loader = async ({ params }: LoaderFunctionArgs) => {
  invariant(params.contactId, 'Missing contactId param');
  const contact = await getContact(params.contactId);
  if (!contact) {
    throw new Response('Not Found', { status: 404 });
  }
  return json({ contact });
};
export default function Contact() {
  const { contact } = useLoaderData<typeof loader>();
  return (
    <div id="contact">
      <div>
        <img
          alt={`${contact.first} ${contact.last} avatar`}
          key={contact.avatar}
          src={contact.avatar}
        />
      </div>
      <div>
        <h1>
          {contact.first || contact.last ? (
            <>
              {contact.first} {contact.last}
            </>
          ) : (
            <i>No Name</i>
          )}{' '}
          <Favorite contact={contact} />
        </h1>
        {contact.twitter ? (
          <p>
            <a href={`https://twitter.com/${contact.twitter}`}>
              {contact.twitter}
            </a>
          </p>
        ) : null}
        {contact.notes ? <p>{contact.notes}</p> : null}
        <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>
          <Form
            action="destroy"
            method="post"
            onSubmit={(event) => {
              const response = confirm(
                'Please confirm you want to delete this record.'
              );
              if (!response) {
                event.preventDefault();
              }
            }}
          >
            <button type="submit">Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}
const Favorite: FunctionComponent<{
  contact: Pick<ContactRecord, 'favorite'>;
}> = ({ contact }) => {
  const favorite = contact.favorite;
  return (
    <Form method="post">
      <button
        aria-label={favorite ? 'Remove from favorites' : 'Add to favorites'}
        name="favorite"
        value={favorite ? 'false' : 'true'}
      >
        {favorite ? '' : ''}
      </button>
    </Form>
  );
};
Enter fullscreen mode Exit fullscreen mode

The code above sets up a contact details page. Spin up your app, and you should have something like this: Example Contact Details Page Set Up In Demo Remix Pwa App Showing Contact Photo, Name, Info, And Buttons To Edit Or Delete. Also Shows List Of Contacts Displayed On Left Sidebar Below Search Bar And Button To Create New Contact As you can see, we’ve set up a simple contacts management app using Remix. Selecting a contact displays details about that contact and allows you to edit the information or delete the contact. You can also use the search bar to filter your list of contacts.

Now that we have our Remix app set up, let’s see how to add PWA features.

Adding PWA features to our Remix app

We’ll use the remix-pwa package to integrate PWA into our Remix application. As we discussed before, this lightweight package allows us to transform our web app into a Remix PWA with very little effort and few configurations.

The remix-pwa package provides a scaffolding template that adds the needed PWA files to our application, including a service worker and a manifest.json file. Let’s explore these in a little more detail.

A service worker is a script that runs in the background and allows our application to work offline. Simply put, it allows offline access to your application. Here’s a snippet of what a service worker looks like:

// src/index.tsx

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then((registration) => {
      console.log('Service Worker registered:', registration.scope);
    })
    .catch((error) => {
      console.error('Service Worker registration failed:', error);
    });
}
Enter fullscreen mode Exit fullscreen mode

Meanwhile, a web app manifest is a JSON file that defines your PWA’s metadata, including its name, icons, and other properties. After installing remix-pwa, you can find this file in your routes directory.

Remix PWA integrates the manifest.json file into the service worker setup automatically during the installation process. Here’s a snippet of what a manifest.json file looks like:

{
  "name": "My Remix PWA",
  "short_name": "Remix PWA",
  "description": "A Remix Progressive Web App",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

We’ve explored the PWA features we want to add to our Remix app. Now, let’s go through the specific steps to install and configure these features using the remix-pwa package.

Installing and configuring remix-pwa

To install the remix-pwa package, run the command below:

npx remix-pwa@latest init
Enter fullscreen mode Exit fullscreen mode

This command will install the Remix CLI, which we will use to scaffold a new Remix PWA application. Follow the prompts and select the PWA features or options you want to integrate into your application, like so: Remix Pwa Framwork Installation With Configuration Options Selected As you can see above, these options include the language, workbox integration, precaching, and so on.

After installation, we need to configure or update our Remix configuration to use our service worker. In your remix.config.ts file, add the following to the top of your file:

/** @type {import('@remix-pwa/dev').WorkerConfig} */
Enter fullscreen mode Exit fullscreen mode

This configuration allows Remix PWA to work with your Remix application.

Next, head over to your entry.client.tsx file and add the code below:

// entry.client.tsx

import { loadServiceWorker } from "@remix-pwa/sw";

loadServiceWorker();
Enter fullscreen mode Exit fullscreen mode

This registers your service worker so that your application can locate your service worker file, which is the entry.worker.tsx file in the routes directory.

Next, in your root.tsx file, import the useSWEffect Hook. This Hook shares some similarities with the useEffect Hook, but is used specifically to listen and inform your service worker of any changes or events:

// root.tsx
import { useSWEffect } from "@remix-pwa/sw";

export default function App() {
  useSWEffect();

return (
  <html lang="en">
    ....
  </html>
);
}
Enter fullscreen mode Exit fullscreen mode

Note that, as is the rule with any other Hook, you have to call the Hook from inside your function component.

Continuing to work inside your root.ts file, replace the existing LiveReload component with a new LiveReload component imported from "@remix-pwa/sw":

// root.tsx 
import { useSWEffect, LiveReload } from "@remix-pwa/sw";
....
Enter fullscreen mode Exit fullscreen mode

This LiveReload component performs the same function as the Remix LiveReload component, but provides support for Remix PWA. It basically listens to file changes and reloads the page. In our Remix PWA’s case, it also updates the service worker.

Lastly, add your manifest path before the Links component within the head tag:

<link rel="manifest" href="/location of your manifest" />
Enter fullscreen mode Exit fullscreen mode

Your completed root file should somewhat look like this:

// root.tsx

import { cssBundleHref } from "@remix-run/css-bundle";
import { useSWEffect, LiveReload } from "@remix-pwa/sw";
import type { LinksFunction } from "@remix-run/node";
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

// ...

export default function App() {
  useSWEffect();

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <link rel="manifest" href="/manifest.webmanifest" />
        <Links />
      </head>
      <body>
      // ....
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

If your manifest is saying that the route is not matching, I recommend that you move your manifest.webmanifest.ts file to the root of your file. Then, make sure your link path is accurately specified as well.

Another common issue is the inaccurate import of buffer in node_modules. If you experience this, head over to the node_modules/@remix-pwa/cache/dist/src/cache.js file and replace your buffer import with the following:

// node_modules/@remix-pwa/cache/dist/src/cache.js

import * as B from 'buffer/index.js';
Enter fullscreen mode Exit fullscreen mode

Finally, build and serve your application by running these commands:

// build your application
npm run build

// serve your application
npm run start
Enter fullscreen mode Exit fullscreen mode

That concludes it. We now have a working Remix PWA. You can inspect the Application tab in your web browser to see your app manifest and service workers up and running. Here’s what you should see in the Service workers sub-tab: Browser Devtools Open To Inspect Application Tab With Service Workers Subtab Open To Show Service Workers Up And Running Meanwhile, here’s what you should see in your App Manifest sub-tab: Browser Devtools Open To Inspect Application Tab With App Manifest Subtab Open In the Lighthouse tab, you can also run the PWA analysis to be sure your application is compatible with PWA requirements: Browser Devtools Open To Inspect Application Tab With Lighthouse Subtab Open To Show Analysis Of App Compatibility With Pwa Requirements

Turning your PWA into a mobile-like app

Earlier, we discussed how one of the benefits of PWAs is that they leverage native mobile features, meaning they can look and behave like mobile apps even though they’re web apps. Right now, our contacts management app still looks like a web app, but it’s easy to create a mobile-like version. Let’s see how.

After building your project, checking that the manifest and service workers are up and running, and running your analysis in the Lighthouse tab, you can install your PWA by clicking on the Install button on the right side of your search input bar. This will create a mobile-like app for your website.

The result should look something like the below, which feels similar to a native app: Native Mobile Like Display Of Remix Pwa Project That’s it! You can also check out the full code for this project on GitLab.

Building PWAs with Remix vs. React

Remix has gained traction and popularity for its developer-friendly and performance approach. However, it’s also good to note some of the limitations associated with building a PWA with Remix.

Having built PWAs with React, I can say it edges over Remix in terms of complexity and developer experience. While the Remix PWA framework makes it easier to build a PWA with Remix, building a PWA with React can be more straightforward and easier to implement in comparison.

First of all, the learning curve of Remix PWA isn’t as easy as React’s. Since Remix is relatively new, you might run into some issues, as highlighted in this article.

Also, while the Remix community is growing, it’s still smaller than frameworks like React or Next.js. This means that the community support for building PWAs with Remix will be small too. If you have questions while working on your project, it may not be easy to find answers or solutions.

Since Remix has a smaller community, the availability of other articles and documentation is also limited. There are very few tutorials on how to build a PWA with Remix, so that might be challenging for developers. React, in comparison, has several articles and tutorials along with comprehensive docs.

Meanwhile, when you run into bugs and issues while working on a React PWA, there is a huge, thriving developer ecosystem available to help out.

Finally, Remix has a great DX, as it handles code-splitting, SSR, and more automatically, making it easier to develop highly performant apps. On the other hand, building a PWA with React doesn’t require several configurations as we’ve seen in this tutorial, which can provide a smoother DX.

As usual, when it comes to choosing the right option for your project, it’s important to assess your needs and go with the option that suits them best. You can use this table as a quick reference regarding the differences and similarities between building a PWA with React vs. using Remix PWA:

Remix React
Learning curve Easier with handling complex features More straightforward and easier to implement
Community Growing, but smaller than React’s Large and thriving community
Documentation/tutorials Limited Extensive
DX Great — automatically handles many features, including route-based data fetching and error handling Great — doesn’t require as many configurations as Remix PWA
Code splitting support Yes, handled automatically Supports code splitting but requires additional setup
SSR Yes, handled automatically Requires additional config
Performance Well-optimized by default Not optimized by default
Routing mechanism SSR CSR; also supports SSR
Data handling Automatically handles simple data Flexible data handling. Also supports plugins or third party intergrations like Redux to enhance data handling capabilities
Best for… Projects that needs automatic data handling Any complex or big projects. BMW, Twitter, Starbucks, and other well-known companies have all used React PWA

Conclusion

In this article, we looked at what a progressive web app is, why it’s important to know how to build a Remix PWA, and how to do so using the remix-pwa package.

Building a PWA can give your users a better and more engaging web experience. Combining Remix’s capabilities with PWA features is a great way to support this purpose.

However, depending on your needs, you may find React to be the better choice for your PWA project. It’s important to look at the complexity, DX, and flexibility of each tool to determine which one will best support you in developing fast, performant PWAs with all the features you need.

I hope you enjoyed this article. If you have any questions, feel free to comment them below. All the best!


Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (0)