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
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

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.