Reputation: 1123
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.
Upvotes: 5
Views: 4915
Reputation: 2160
Following on from Kiliman's updated answer, here's how I did it using the micro-memoize package with TypeScript:
import type memoize from "micro-memoize";
import { singleton } from "~/utils/singleton.server";
type Memoizer = typeof memoize;
const memoizer: Memoizer = singleton("memoize", () => require("micro-memoize"));
export { memoizer as memoize };
Hopefully this helps anyone who's having the same issue and wants to use something more modern than node-cache
.
Thanks again to Kiliman for providing the updated Singleton function.
Upvotes: 1
Reputation: 20322
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.
EDIT: Here's a better way to handle this
// utils/singleton.server.ts
// since the dev server re-requires the bundle, do some shenanigans to make
// certain things persist across that 😆
// Borrowed/modified from https://github.com/jenseng/abuse-the-platform/blob/2993a7e846c95ace693ce61626fa072174c8d9c7/app/utils/singleton.ts
export function singleton<Value>(name: string, value: () => Value): Value {
const yolo = global as any
yolo.__singletons ??= {}
yolo.__singletons[name] ??= value()
return yolo.__singletons[name]
}
// utils/prisma.server.ts
import { PrismaClient } from '@prisma/client'
import { singleton } from './singleton.server.ts'
const prisma = singleton('prisma', () => new PrismaClient())
prisma.$connect()
export { prisma }
Upvotes: 10
Reputation: 960
I did this to have it at the server side as well. Make a file with the following content and call it wherever you want to use it.
import NodeCache from 'node-cache';
let cache: NodeCache;
declare global {
var __cache: NodeCache | undefined;
}
export default function getNodeCache() {
if (process.env.NODE_ENV === 'production') {
cache = new NodeCache();
} else {
if (!global.__cache) {
global.__cache = new NodeCache();
}
cache = global.__cache;
}
return cache;
}
Upvotes: 0
Reputation: 490
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);
}
Upvotes: 6