Load data from a file in Remix and Vercel
TL;DR
For some weird reason, you cannot read files from Remix the same way you read files from Next.js on Vercel.
The workaround is to use fs.readFile(__dirname + '/../json/data.json', 'utf8')
. Instead if using process.cwd()
and path.join()
.
This workaround was found the hard way by a couple of people mentioned below, however, it's not documented properly and escaped me for months.
Here goes my attempt to spread this workaround until it's officially documented or fixed by Vercel.
Why is this happening?
When I tweeted about this workaround, Vercel's CEO Guillermo Rauch explained the reason beind this behavior:
Guillermo also confirmed that Next.js is doing some magic to make process.cwd()
and path.join()
work.
Below is a detailed guide on how to read files in Remix and Vercel
Set up the json
data
Create a basic Remix.run
rpoject with npx create-remix@latest
and create a json
folder in the root of the project, and inside it, create a data.json
file.
┌── api ├── app ├── json │ └── data.json ├── # ...
Then paste the following code in the data.json
file:
{ "record": { "id": 8221, "uid": "a15c1f1d-9e4e-4dc7-9c45-c04412fc5064", "name": "Remix.run", "language": "TypeScript" } }
Read the file in the route's loader
In app/routes/index.tsx
, add the following code:
import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { promises as fs } from "fs"; export const loader = async (args: LoaderArgs) => { // Find the absolute path of the json directory // Note: As of July 17, 2022, Vercel doesn't include the json directory when using process.cwd() or path.join(). The workaround is to use __dirname and concatenate the json directory to it. const jsonDirectory = __dirname + "/../json"; // Read the json data file data.json const fileContents = await fs.readFile(jsonDirectory + "/data.json", "utf8"); // Parse the json data file contents into a json object const data = JSON.parse(fileContents); return json({ data, }); };
Run your application locally using npm run dev
and browse to http://localhost:3000
and you should see:
{"data":{"record":{"id":8221,"uid":"a15c1f1d-9e4e-4dc7-9c45-c04412fc5064","name":"Remix.run","language":"TypeScript"}}}
Display the data in the route's component
To display the returned data in the route's component, add the following code to the same file:
// ... import { useLoaderData } from '@remix-run/react' // ... export default function Index() { const { data } = useLoaderData<typeof loader>() return ( <div> <h1>My Framework from file</h1> <ul> <li>Name: {data.record.name}</li> <li>Language: {data.record.language}</li> </ul> </div> ) }
You should see the following error in the browser:
Error: Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules. Check this link for more details: https://remix.run/pages/gotchas#server-code-in-client-bundles
This is because the node:fs/promises
module we imported in the route made it into browser bundles as explained in Remix's docs.
To make sure it's used on the server only, create a utils
directory inside app
, and inside it, create fs-promises.server.ts
.
├── # ... ├── app │ └── routes │ └── utils │ └── fs-promises.server.ts ├── # ...
Then export the node:fs/promises
module from the fs-promises.server.ts
file:
export { promises as fs } from "fs";
Now you can import node:fs/promises
from the fs-promises.server.ts
instead as follows:
// ... import { fs } from "~/utils/fs-promises.server"; // ...
When you brose to http://localhost:3000
you should see the same data as on this demo.
That's all!
Here's the source code of the demo that's deployed to Vercel.