When it comes to mobile development, the industry is increasingly moving toward cross-platform technologies like Expo and Lynx. These frameworks allow you to build an application once and ship it to iOS, Android, and the web, saving significant time and resources. However, this convenience comes with its own set of challenges, as cross-platform codebases don't always cater to every platform perfectly. Today, we'll discuss a common issue and a solution, specifically for web deployments.
For readers seeking a quick solution, skip to the summary section below.
I recently began porting the Listii application from a Next.js web app to an Expo app that can run on both mobile and web. My goal was to provide the same seamless experience on both platforms. I chose Expo for its use of React Native for Web, which translates React Native components into their web counterparts. Another benefit was its file-based routing, a feature I was already familiar with from Next.js's app router. This is where I encountered a major hurdle—one you might be facing as well.
The rules for file-based routing in Expo are similar to Next.js's app router:
-
Each file in the
app
directory maps to a page and must have a default export that defines the page’s UI. -
Directories within the
app
directory map to groups of related screens. -
The initial route (first screen) is
index.tsx
. -
You can use a
_layout.tsx
file to define behavior for a group of related screens, such as a splash screen. -
Each page in the application can be navigated to via a URL.
For this project, I’m using static rendering, an architecture where content is generated during the build process on the server. Expo's static rendering outputs each page as a .html
file. This works great for static paths, but it requires knowing all the paths at build time, which isn't possible with dynamic routes generated by user content at runtime.
This poses a problem: if the list of dynamic routes isn't provided at build time—for example, by not calling the generateStaticParams
function—dynamic routes like /lists/[list_name]
will be output as /lists/[list_name].html
. When a user navigates to a dynamic route through the app's UI, the page might initially render correctly. However, if they refresh the page or try to navigate directly to the URL, they'll get a 404 error.
This is my current understanding of the problem. If you have any suggestions or alternative solutions, please reach out to me on my social media pages.
Since I'm using Netlify to host the web version of the project, the solution is specific to that platform. I worked around this problem by redirecting all routes to index.html
and handling the dynamic routing on the client side with JavaScript.
To do this, I went to the public
folder of my project and created a file named _redirects
with the following content:
/* /index.html 200
This line tells Netlify to serve the index.html
file for any route a user tries to access on the website. This allows the client-side JavaScript to take control and display the correct content based on the URL, effectively bypassing the server's static routing limitations for dynamic paths.
Summary of Steps
-
Navigate to your
public
folder. If it doesn't exist, create it in your project's root directory. -
Create a new file inside the
public
folder and name it_redirects
. -
Add the following line to the file:
/* /index.html 200
-
Deploy your project. This simple redirect rule will now handle all dynamic routes, preventing 404 errors on direct access or page refresh.