remix icon indicating copy to clipboard operation
remix copied to clipboard

feat(remix-server-runtime): pass `AppLoadContext` to the server's `handleRequest` function

Open axel-habermaier opened this issue 3 years ago β€’ 18 comments

The AppLoadContext instance can optionally be set via the server adapter's getLoadContext() function. It is subsequently passed to all actions, loaders as well as the handleDataRequest function. It is, however, not passed to the handleRequest function.

This PR also passes the load context to the handleRequest function so that the context is available everywhere in a consistent manner.

  • [x] Docs
  • [x] Tests

axel-habermaier avatar Feb 19 '22 19:02 axel-habermaier

Thank you for signing the Contributor License Agreement. Let's get this merged! πŸ₯³

remix-cla-bot[bot] avatar Mar 28 '22 06:03 remix-cla-bot[bot]

Hmm.. not sure why it's not working for you. I setup a codesandbox that uses getLoadContext and I am able to get the value from loader({context}) https://codesandbox.io/s/remix-getloadcontext-f8s1s0?file=/app/routes/index.tsx

// server/index.js

const getLoadContext = req => ({
  test: `This is from getLoadContext: ${new Date(Date.now()).toISOString()}`,
})

function handleRequest(req, res, next) {
  let build = require('./build')
  if (MODE !== 'production') {
    purgeRequireCache()
  }
  return createRequestHandler({
    build,
    getLoadContext,
    mode: MODE,
  })(req, res, next)
}

kiliman avatar Mar 28 '22 15:03 kiliman

@kiliman: Thanks for the example. But this is not what this PR does. The load context is already available to all loader, and action functions, as your example shows. It is not, however, available in the function that renders your React component tree on the server in entry.server.tsx. From the PR's diff:

grafik

This PR adds the load context as the last parameter of the default function in entry.server.tsx. That way, the change is API backward compatible (because in JavaScript, you can always omit unused parameters in functions that get called with more parameters).

axel-planfox avatar Mar 29 '22 05:03 axel-planfox

Hmm. Sorry, I guess I misunderstood what you were doing. Ok.

The handleDataRequest export API came later. I believe the person that implemented the export used the same API as the internal handler, therefore loadContext came along for the ride. I'm not sure that was the intention as the context is already being passed to the loader directly. It was added in this commit https://github.com/remix-run/remix/commit/2f4d8fdc527f8f8b3c7c3fe8c9e14cbb486729b8 (server-runtime/server.js lines 158-163)

But the component is also rendered on the client. Your context contains information from your adapter to be used by your loaders and actions. If there's anything in context that is needed by the component, it should be returned by the loader/action.

kiliman avatar Mar 29 '22 21:03 kiliman

The problem is that I can't rely on loaders to provide data - they're don't, for instance, in case of 404s or top-level errors. Nevertheless, we need some tenant-specific info in these cases (tenant name, logo, URL, language, color theme, and even some user-specific stuff, etc.). Since we can't pass this info via loaders in all cases, we have to partially re-implement Remix' encoding of loader data in the HTML, in our case encoding the tenant info in the topmost component into the HTML, wrapping <RemixServer/> in entry.server.tsx. In entry.client.tsx, we do the opposite and wrap <RemixClient/> in a component that reads the tenant info from the HTML and makes it available everywhere via a React context.

We would like to uniformly pass the tenant-specific data in entry.server.tsx via the load context, that is normally used by the loaders to pass the data to the frontend. Right now, we have to use an ugly workaround with global variables and a per-request look-up table for the correct context, requiring this context to be removed from the lookup when the requests ends to avoid memory leaks.

Obviously, if we can't even load the tenant info due to errors, we have to fall back to an unstyled, tenant-unspecific error. But that is extremely unlikely, as our tenant info is stored in-memory and there are not many possibilities for failures. 404s and other internal server errors, however, are likely to happen, and we have to provide a tenant-specific errors in these cases.

Having said that - that's our use case. I can imagine other reasons why making the load context available everywhere on the server-side is a good idea. As a matter of fact, it is already available everywhere, except in this one specific function. This PR fixes that.

axel-planfox avatar Mar 30 '22 06:03 axel-planfox

@axel-xitaso What about getting the tenant info in the loader of the root? That way you would have it available too without needing this PR.

MichaelDeBoey avatar Mar 30 '22 15:03 MichaelDeBoey

@MichaelDeBoey : Thanks for the suggestion. According to my tests, useLoaderData returns undefined in the root's catch and error boundaries (when rendered server-side, but interestingly, not on client navigations), but that's where we would (also) need this data. Or am I missing something?

Update: Remix event prints an error You cannot useLoaderData in an error boundary., so potentially it's a bug that it doesn't work for sever-side rendered top-level catch boundaries? In any case, we also need the tenant info in the global error boundary, so this PR is still necessary for us.

axel-planfox avatar Mar 30 '22 16:03 axel-planfox

Are there any plans to get this merged?

If so, let me know and I'll rebase this PR onto the latest dev. Otherwise, I won't waste my time by doing so repeatedly without any indication whatsoever if this PR will ever get merged.

axel-habermaier avatar Apr 08 '22 17:04 axel-habermaier

I can imagine other reasons why making the load context available everywhere on the server-side is a good idea. As a matter of fact, it is already available everywhere, except in this one specific function. This PR fixes that.

Just chiming-in to say I also found myself looking for load context in entry.server.tsx's handleRequest() today. I was surprised/disappointed to find that it's not available.

This PR fixes what appears to be an oversight in the API; I cannot think of a good reason to deliberately not share the load context with entry.server.tsx when it's available everywhere else that related to the request cycle. I would love to see this PR updated and accepted πŸ‘

Thanks @axel-habermaier! :)

tnightingale avatar May 11 '22 22:05 tnightingale

Adding my additional comment that this is important for me as well. I require an Apollo client in handleRequest which is being created in my server.ts and passed into the loadContext.

Personally, I cannot think of any reason not to expose the loadContext to the handleRequest.

ZipBrandon avatar May 21 '22 15:05 ZipBrandon

@axel-habermaier Could you please rebase onto latest dev & resolve conflicts?

MichaelDeBoey avatar May 21 '22 15:05 MichaelDeBoey

@MichaelDeBoey: Done.

axel-habermaier avatar May 21 '22 17:05 axel-habermaier

Despite my earlier concerns, I will +1 this PR. I think it will be useful for cases where loaders/actions fail. This will be the last opportunity to affect the response that depends on context.

kiliman avatar May 21 '22 19:05 kiliman

These are odd failures in theπŸ§ͺ Test / πŸ§ͺ Test OS: windows-latest for Node 14 and Node 18 which contrasts to Node 16 working.

ZipBrandon avatar Jun 02 '22 19:06 ZipBrandon

@mjackson Getting visibility of this since it did not make it in v1.6.0

blorenz avatar Jun 15 '22 14:06 blorenz

Windows having that odd test failure again which was previously resolved by a force push. -- πŸ§ͺ Test / πŸ§ͺ Test: (OS: windows-latest Node: 14) (pull_request)

ZipBrandon avatar Jun 21 '22 19:06 ZipBrandon

Discovered this PR while attempting to pass in Cloudflare Pages/Workers environment bindings (e.g. KV, DurableObjects) to the server's entry point. Eager to see this change merged in and released.

awwong1 avatar Aug 04 '22 22:08 awwong1

I've also discovered this PR while trying to pass in an instance of our logger from the express server into remix. I can get access to it just fine in actions and loaders, but, I want access to it in entry.server as well.

marwan38 avatar Oct 25 '22 00:10 marwan38

Any movement on this?... we need this as well. πŸ‘

sciutand avatar Nov 16 '22 19:11 sciutand

⚠️ No Changeset found

Latest commit: 269eb96ec7cbe2c774797af7b68c4992cc57ad57

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

changeset-bot[bot] avatar Nov 16 '22 19:11 changeset-bot[bot]

PR needs to be updated after the 1.8.0 changes.

marwan38 avatar Dec 04 '22 21:12 marwan38

@axel-habermaier Can you please rebase onto latest dev, resolve conflicts & fix tests?

MichaelDeBoey avatar Dec 04 '22 21:12 MichaelDeBoey

This PR has been automatically closed because we haven't received a response from the original author πŸ™ˆ. This automation helps keep the issue tracker clean from PRs that are unactionable. Please reach out if you have more information for us! πŸ™‚

github-actions[bot] avatar Dec 15 '22 07:12 github-actions[bot]

😞

sergiodxa avatar Dec 15 '22 22:12 sergiodxa

Re-opening this one as we were only a rebase away

@axel-habermaier Please rebase this branch onto latest dev & fix conflicts so we can merge this one.

@sergiodxa I think you can create a resubmission PR as well if you want to get this one merged.

MichaelDeBoey avatar Dec 15 '22 22:12 MichaelDeBoey

@MichaelDeBoey: I opened this PR almost a year ago and already rebased it twice without it getting merged. Since I've completely lost all interest in Remix in the last couple of months, I won't put any more time into this PR.

Sorry for that, but since the changes are trivial, someone else should easily be able to open a new PR.

axel-habermaier avatar Dec 17 '22 19:12 axel-habermaier

@sergiodxa If you want, please open a new PR to implement this

MichaelDeBoey avatar Dec 17 '22 21:12 MichaelDeBoey

Do you plan to fix this in a future release? Are someone willing to reopen this PR? We are new to Remix (coming from React Router v6) and got stuck immediately after installation since we can't share our Apollo client instance with loaders and actions functions. This is confusing as getLoadContext is a documented feature but not enabled atm.

Are you tempering this because of this proposal on React Router https://github.com/remix-run/react-router/discussions/9564 which is likely to be brought back over to Remix?

In the mean time I think this is really a blocker for some users who need dependencies in their loaders or actions functions. Server side and client side.

benjamindulau avatar Dec 28 '22 15:12 benjamindulau

I was planning on recreating this PR but I’m on holiday atm. Perhaps I can get sometime soon to do it. We also need it but I’ve patched it on our side as a temp work around using npm patch package.

marwan38 avatar Dec 28 '22 15:12 marwan38

@marwan38 Please bear with me since I'm not yet familiar with the Remix code base :-) But how did you patched it? I tried to apply a patch based on this PR, but I don't get how to pass the context to the <RemixServer /> component in the following server entry code:

// entry.server.tsx
import { PassThrough } from "stream";
import type { EntryContext } from "@remix-run/node";
import { Response } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5000;

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  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 didError = false;

    const { pipe, abort } = renderToPipeableStream(
      <RemixServer context={remixContext} url={request.url} />,
      {
        onAllReady() {
          const body = new PassThrough();

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(body, {
              headers: responseHeaders,
              status: didError ? 500 : responseStatusCode,
            })
          );

          pipe(body);
        },
        onShellError(error: unknown) {
          reject(error);
        },
        onError(error: unknown) {
          didError = true;

          console.error(error);
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}

function handleBrowserRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    let didError = false;

    const { pipe, abort } = renderToPipeableStream(
      <RemixServer context={remixContext} url={request.url} />,
      {
        onShellReady() {
          const body = new PassThrough();

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(body, {
              headers: responseHeaders,
              status: didError ? 500 : responseStatusCode,
            })
          );

          pipe(body);
        },
        onShellError(err: unknown) {
          reject(err);
        },
        onError(error: unknown) {
          didError = true;

          console.error(error);
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}

Plus, our apollo client instances are not configured exactly the same between server and client, how would you pass it to loaders context on client side only components?

benjamindulau avatar Dec 28 '22 16:12 benjamindulau