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.
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.
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:
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.
Before we dive into the tutorial part of this article, ensure that you have the following installed:
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.
We will start by creating a new Remix project. Run the command below:
npx create-remix@latest
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
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:
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
I’m not using a template, so you can use this command to install your Remix app:
npx create-remix@latest
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> ); }
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> ); };
The code above sets up a contact details page. Spin up your app, and you should have something like this:
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.
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); }); }
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" } ] }
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.
remix-pwa
To install the remix-pwa
package, run the command below:
npx remix-pwa@latest init
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:
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} */
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();
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> ); }
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"; ....
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" />
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> ); }
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';
Finally, build and serve your application by running these commands:
// build your application npm run build // serve your application npm run start
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:
Meanwhile, here’s what you should see in your App Manifest sub-tab:
In the Lighthouse tab, you can also run the PWA analysis to be sure your application is compatible with PWA requirements:
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:
That’s it! You can also check out the full code for this project on GitLab.
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 |
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!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.