How do I use in-memory cache in Remix.run dev mode?

Viewed 422

I need to fetch data from an API that is pretty slow and seldom changes, so I thought I'd use an in-memory cache. I first tried a very simple approach by just saving it to a variable outside the scope of the loader function in my route:

let cache;

export const loader = async () => {
  if (!cache) {
    // we always end up here
    cache = await (await fetch("...)).json()
  }
}

but that didn't work. I then tried a proper caching library (lru-cache), but that cache was also always empty. I then realized that the entired file got reloaded on each request which I guess is a dev mode thing, so I tried moving the creation of the cache to a separate file cache.server.ts and importing it from there.

import LRU from "lru-cache";
console.log("Creating cache"); // this is logged on each request
const cache = new LRU({ max: 200 });
export default cache;

But that file also seems to be reloaded on each request.

If I build a production version and run that everything works great, but it would be nice to have some way of getting it to work in dev mode as well.

2 Answers

Remix purges the require cache on every request in development to support <LiveReload/>. To make sure your cache survives these purges, you need to assign it to the global object.

Here's an example from the Jokes Tutorial

import { PrismaClient } from "@prisma/client";

let db: PrismaClient;

declare global {
  var __db: PrismaClient | undefined;
}

// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
if (process.env.NODE_ENV === "production") {
  db = new PrismaClient();
} else {
  if (!global.__db) {
    global.__db = new PrismaClient();
  }
  db = global.__db;
}

export { db };

https://remix.run/docs/en/v1/tutorials/jokes#connect-to-the-database

As a follow up on Kilimans answer, here's how I did it:

/*
 * @see https://www.npmjs.com/package/node-cache
 */
import NodeCache from "node-cache";
let cache: NodeCache;

declare global {
  var __cache: NodeCache | undefined;
}

if (process.env.NODE_ENV === "production") {
  cache = new NodeCache();
} else {
  if (!global.__cache) {
    global.__cache = new NodeCache();
  }
  cache = global.__cache;
}

export { cache };

And I used it in the loader:

import { getGitHubRepos } from "~/models/github.server";
import { cache } from "~/utils/cache";

export async function loader(args: LoaderArgs) {
  if (cache.has("GitHubRepos")) {
    return json(cache.get("GitHubRepos"));
  }
  const repos = await getGitHubRepos();
  cache.set("GitHubRepos", repos, 60 * 60 * 24);
  return json(repos);
}
Related