code-forge-io / code-forge-io/remix-client-cache

Support caching for streamed responses

Open
#7 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
247
Forks
3
PR merge metrics
No merged PRs in 30d

Description

[Stackblitz](https://stackblitz.com/~/github.com/predaytor/remix-test?file=app/routes/test.tsx)

When using `defer` to stream a response from a loader that returned a promise which later used in a boundary, we cannot properly serialize the promise to store say in localStorage or IndexedDB (`JSON.stringify` will result in an empty object `{}`). The only way to store this type of data is directly in memory, so on the next navigation, `clientLoader` will return fulfilled promise with the data, rather than calling the server `loader'.
```tsx
let cache: SerializeFrom;
export const clientLoader = defineClientLoader(async ({ serverLoader }) => {
if (!cache) {
cache = await serverLoader();
}

return cache;
});

clientLoader.hydrate = true;
```
By using `remix-client-cache`, we can create an adapter for when we need to cache streaming responses, rather than relying on a global setting using, for example, `localStorage`.
```ts
import { lru } from 'tiny-lru';
import { type CacheAdapter, createCacheAdapter } from 'remix-client-cache';

const cache = lru(100);

class LRUAdapter implements CacheAdapter {
async getItem(key: string) {
return cache.get(key);
}

async setItem(key: string, value: any) {
return cache.set(key, value);
}

async removeItem(key: string) {
return cache.delete(key);
}
}

export const { adapter: lruAdapter } = createCacheAdapter(() => new LRUAdapter());
```

`routes/index.tsx`:
```tsx
import { defer } from '@remix-run/node';
import { Await, ClientLoaderFunctionArgs, Link } from '@remix-run/react';
import { Suspense } from 'react';
import { cacheClientLoader, useCachedLoaderData } from 'remix-client-cache';
import { lruAdapter } from '~/client-cache-adapter';

async function getQueryData() {
await new Promise((resolve) => setTimeout(resolve, 3000));
return { data: [{ id: 1 }] };
}

export async function loader() {
const query = getQueryData();

return defer({
query,
});
}

export const clientLoader = (args: ClientLoaderFunctionArgs) => cacheClientLoader(args, { adapter: lruAdapter });
clientLoader.hydrate = true;

export default function Page() {
const { query } = useCachedLoaderData();

return (
<>
Home






{({ data }) => (


    {data?.map((entry) =>
  • {entry.id}
  • )}

)}



);
}
```
This opens up a question whether it's a good idea to store data in server memory, it would be interesting if we could transform fulfilled promises using a library like [`turbo-stream`](https://github.com/jacob-ebey/turbo-stream) to store on the client-side or use a web worker and decode to the original form for consumption by .

---

There is currently a bug where a returned promise from the cache has already been fulfilled, the internal logic of `remix-client-cache` cannot understand when a revalidation should occur, or fulfilled data is currently present, since we must store promises directly in memory and not as a string.
https://github.com/forge42dev/remix-client-cache/blob/main/src/index.tsx#L116-L140
```tsx
// Unpack deferred data from the server
useEffect(() => {
let isMounted = true;
if (loaderData.deferredServerData) {
loaderData.deferredServerData.then((newData: any) => {
if (isMounted) {
adapter.setItem(loaderData.key, newData);
setFreshData(newData);
}
});
}
return () => {
isMounted = false;
};
}, [loaderData, adapter]);

// Update the cache if the data changes
useEffect(() => {
if (
loaderData.serverData &&
JSON.stringify(loaderData.serverData) !== JSON.stringify(freshData)
) {
setFreshData(loaderData.serverData);
}
}, [loaderData?.serverData, freshData]);
```

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/index.tsx lines 116-140 and reproduce the fulfilled-promise path from the linked StackBlitz/routes/index.tsx example. Trace how deferredServerData, serverData, freshData, and adapter are compared; done means the cache distinguishes fulfilled cached data from data requiring revalidation with verified behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.