Apollo with Remix 2.0? #7661
Replies: 4 comments 1 reply
-
The followings work for me for Remix 2.2 . I had a plan to create a Remix Stack, but maybe later :). To see changes, you can compare the file with the origin ones here: https://github.com/remix-run/remix/tree/main/templates/remix/app for the entery.server.tsx// entery.server.tsx file
/**
* By default, Remix will handle generating the HTTP Response for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.server
*/
import { PassThrough } from "node:stream";
import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import {
ApolloProvider,
ApolloClient,
InMemoryCache,
createHttpLink,
} from "@apollo/client";
import { getDataFromTree } from "@apollo/client/react/ssr";
import { authenticator } from "./services/auth.server";
import type { ReactElement } from "react";
const ABORT_DELAY = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext,
) {
return isbot(request.headers.get("user-agent"))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise(async (resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
await wrapRemixServerWithApollo(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
request,
),
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
setTimeout(abort, ABORT_DELAY);
});
}
async function wrapRemixServerWithApollo(
remixServer: ReactElement,
request: Request,
) {
const client = await getApolloClient(request);
const app = <ApolloProvider client={client}>{remixServer}</ApolloProvider>;
await getDataFromTree(app);
const initialState = client.extract();
const appWithData = (
<>
{app}
<script
dangerouslySetInnerHTML={{
__html: `window.__APOLLO_STATE__=${JSON.stringify(
initialState,
).replace(/</g, "\\u003c")}`, // The replace call escapes the < character to prevent cross-site scripting attacks that are possible via the presence of </script> in a string literal
}}
/>
</>
);
return appWithData;
}
async function getApolloClient(request: Request) {
const client = new ApolloClient({
ssrMode: true,
cache: new InMemoryCache(),
link: createHttpLink({
uri: 'http://localhost:3000/graphql',
headers: {
...Object.fromEntries(request.headers),
},
credentials: request.credentials ?? "include", // or "same-origin" if your backend server is the same domain
}),
});
return client;
} for the entery.client.tsx// entery.client.tsx
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.client
*/
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
import { ThemeProvider } from "@material-tailwind/react";
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
const client = new ApolloClient({
cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
uri: 'http://localhost:3000/graphql'
});
hydrateRoot(
document,
<StrictMode>
<ApolloProvider client={client}>
<ThemeProvider>
<RemixBrowser />
</ThemeProvider>
</ApolloProvider>
</StrictMode>,
);
}); |
Beta Was this translation helpful? Give feedback.
-
I think you’re asking two questions in one:
We should approach these questions separately, i.e. you should determine first whether you need Apollo client at all. The idiomatic approach to fetch a GraphQL endpoint with Remix is, just like with any other endpoint, to fetch your data in a
If you really need to use Apollo Client (e.g. you want to make atomic changes to your cache and avoid refetching the entire client state upon mutation), I’d recommend instantiating Apollo Client only for those routes that need it, and not wrapping your entire app in it. In these routes, you will not use the Remix I did this for my app as well, and although the setup works, I’m wondering if the additional complexity is worth it. You’d instantiate Apollo Client in the loader function (with Going this approach, you won’t instantiate your Apollo Client outside the component scope (as per their docs), and you won’t be setting any data on the global (
Hope this helps. |
Beta Was this translation helpful? Give feedback.
-
@mahmoudmoravej Thanks your sample code, i followed all steps, but it shows a general error if there is a react-select component
Hope this detailed. |
Beta Was this translation helpful? Give feedback.
-
Has anyone found a way to properly run "real-world" Apollo with Remix, caching and everything? I tried something and can't get caching to work (https://github.com/vladinator1000/pages-ssr-apollo). This is making me want to give up on Apollo and go straight for remix loader/clientLoader (https://youtu.be/MrDhjB5ucHI). |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Starting a project to hit a graphql server. All the graphql examples on Youtube and Medium are about a year old and their approaches don't match with how Remix has evolved.
Can someone share how to setup Apollo client to work with Remix@latest?
Beta Was this translation helpful? Give feedback.
All reactions