Giter Site home page Giter Site logo

reverecre / relay-nextjs Goto Github PK

View Code? Open in Web Editor NEW
251.0 9.0 30.0 1.75 MB

⚡️ Relay integration for Next.js apps

Home Page: https://reverecre.github.io/relay-nextjs/

License: MIT License

TypeScript 73.82% JavaScript 21.11% CSS 5.07%
next nextjs relay relay-hooks graphql relay-environment

relay-nextjs's Introduction

Revere CRE is hiring! Interested in working on the cutting edge of frontend?
Reach out to [email protected] for more information.

Relay + Next.js

npm version npm downloads npm bundle size

Documentation | Discussion | Latest Releases

relay-nextjs is the best way to use Relay and Next.js in the same project! It supports incremental migration, is suspense ready, and is run in production by major companies.

Overview

relay-nextjs wraps page components, a GraphQL query, and some helper methods to automatically hook up data fetching using Relay. On initial load a Relay environment is created, the data is fetched server-side, the page is rendered, and resulting state is serialized as a script tag. On boot in the client a new Relay environment and preloaded query are created using that serialized state. Data is fetched using the client-side Relay environment on subsequent navigations.

Note: relay-nextjs does not support Nextjs 13 App Router at the moment.

Getting Started

Install using npm or your other favorite package manager:

$ npm install relay-nextjs

relay-nextjs must be configured in _app to properly intercept and handle routing.

Setting up the Relay Environment

For basic information about the Relay environment please see the Relay docs.

relay-nextjs was designed with both client-side and server-side rendering in mind. As such it needs to be able to use either a client-side or server-side Relay environment. The library knows how to handle which environment to use, but we have to tell it how to create these environments. For this we will define two functions: getClientEnvironment and createServerEnvironment. Note the distinction — on the client only one environment is ever created because there is only one app, but on the server we must create an environment per-render to ensure the cache is not shared between requests.

First we'll define getClientEnvironment:

// lib/client_environment.ts
import { Environment, Network, Store, RecordSource } from 'relay-runtime';

export function createClientNetwork() {
  return Network.create(async (params, variables) => {
    const response = await fetch('/api/graphql', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: params.text,
        variables,
      }),
    });

    const json = await response.text();
    return JSON.parse(json);
  });
}

let clientEnv: Environment | undefined;
export function getClientEnvironment() {
  if (typeof window === 'undefined') return null;

  if (clientEnv == null) {
    clientEnv = new Environment({
      network: createClientNetwork(),
      store: new Store(new RecordSource()),
      isServer: false,
    });
  }

  return clientEnv;
}

and then createServerEnvironment:

import { graphql } from 'graphql';
import { GraphQLResponse, Network } from 'relay-runtime';
import { schema } from 'lib/schema';

export function createServerNetwork() {
  return Network.create(async (text, variables) => {
    const context = {
      token,
      // More context variables here
    };

    const results = await graphql({
      schema,
      source: text.text!,
      variableValues: variables,
      contextValue: context,
    });

    return JSON.parse(JSON.stringify(results)) as GraphQLResponse;
  });
}

export function createServerEnvironment() {
  return new Environment({
    network: createServerNetwork(),
    store: new Store(new RecordSource()),
    isServer: true,
  });
}

Note in the example server environment we’re executing against a local schema but you may fetch from a remote API as well.

Configuring _app

// pages/_app.tsx
import { RelayEnvironmentProvider } from 'react-relay/hooks';
import { useRelayNextjs } from 'relay-nextjs/app';
import { getClientEnvironment } from '../lib/client_environment';

function MyApp({ Component, pageProps }: AppProps) {
  const { env, ...relayProps } = useRelayNextjs(pageProps, {
    createClientEnvironment: () => getClientSideEnvironment()!,
  });

  return (
    <>
      <RelayEnvironmentProvider environment={env}>
        <Component {...pageProps} {...relayProps} />
      </RelayEnvironmentProvider>
    </>
  );
}

export default MyApp;

Usage in a Page

// src/pages/user/[uuid].tsx
import { withRelay, RelayProps } from 'relay-nextjs';
import { graphql, usePreloadedQuery } from 'react-relay/hooks';

// The $uuid variable is injected automatically from the route.
const ProfileQuery = graphql`
  query profile_ProfileQuery($uuid: ID!) {
    user(id: $uuid) {
      id
      firstName
      lastName
    }
  }
`;

function UserProfile({ preloadedQuery }: RelayProps<{}, profile_ProfileQuery>) {
  const query = usePreloadedQuery(ProfileQuery, preloadedQuery);

  return (
    <div>
      Hello {query.user.firstName} {query.user.lastName}
    </div>
  );
}

function Loading() {
  return <div>Loading...</div>;
}

export default withRelay(UserProfile, UserProfileQuery, {
  // Fallback to render while the page is loading.
  // This property is optional.
  fallback: <Loading />,
  // Create a Relay environment on the client-side.
  // Note: This function must always return the same value.
  createClientEnvironment: () => getClientEnvironment()!,
  // variablesFromContext allows you to declare and customize variables for the graphql query.
  // by default variablesFromContext is ctx.query
  variablesFromContext: (ctx: NextRouter | NextPageContext) => ({ ...ctx.query, otherVariable: true }),
  // Gets server side props for the page.
  serverSideProps: async (ctx) => {
    // This is an example of getting an auth token from the request context.
    // If you don't need to authenticate users this can be removed and return an
    // empty object instead.
    const { getTokenFromCtx } = await import('lib/server/auth');
    const token = await getTokenFromCtx(ctx);
    if (token == null) {
      return {
        redirect: { destination: '/login', permanent: false },
      };
    }

    return { token };
  },
  // Server-side props can be accessed as the second argument
  // to this function.
  createServerEnvironment: async (
    ctx,
    // The object returned from serverSideProps. If you don't need a token
    // you can remove this argument.
    { token }: { token: string }
  ) => {
    const { createServerEnvironment } = await import('lib/server_environment');
    return createServerEnvironment(token);
  },
});

relay-nextjs's People

Contributors

0xhexe avatar alailsonko avatar elton-okawa avatar ergofriend avatar findarkside avatar gio-work avatar giologist avatar hanford avatar hoangbits avatar monoppa avatar moox avatar pranavsathy avatar robertt avatar rrdelaney avatar supaspoida avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

relay-nextjs's Issues

Create server environment without a query

Hello, first of all, thanks for this package.

I would like to know if there a way to create a server environment without passing the query. I do not want to do the query on the top-level page.

Thanks.

Does not run queries without an argument on first page load

First of all, ¡this is and incredible library!, it works beautifully.

But there is a wired problem (or maybe I'm configured something wrong), the issue is, when I try to load a query with an argument, like an ID or something to filter the query, it works perfectly when I reload the page or start to navigate across the APP, but, when I run a simple querie, without arguments, it throws a null value when the page is reloaded, but here is the thing, when I start to navigate in the APP, the querie without arguments works, and I get the information required.

I'm very new in this of coding and handling github PR's and ISSUES, feel free to redirect me to the correct place to post this, I will really appreciaite your help.

Thank's again for your incredible work!!!!!

Possible to use `useRefetchableFragment` inside of page component?

I'm getting an error and confusing stacktrace when trying to refetch a fragment inside of a withRelay wrapped page. Is this expected? I thought the suspense boundary on the page, would just show a loading and the refetch would succeed, maybe I've got the wrong mental model on what hooks can be used with this library.

Try to expose GraphQL response headers to serverSidePostQuery

It would be great if response headers from the GraphQL request done on server would be exposed to serverSidePostQuery.

Use-case:
Backend can set the cache-control header on GraphQL responses, which could be read in serverSidePostQuery and set on the page:

serverSidePostQuery: (_, ctx) => {
    ctx.res.setHeader(
      'cache-control',
      headerFromBackend
    );
  }

Could try to contribute this, if you think this makes sense and would be open to accepting PR for it.

Use with other server side props

next-auth requires I pass a session prop from the server on page load. Otherwise there's a delay as a call is needed to be made client side. Is it possible to provide a server variable alongside relay-nextjs?

function Home({ preloadedQuery, session }) {
...
}

export default withRelay(Home, HomeQuery, {
...
})

export async function getInitialProps(context) {
  return {
    props: {
      session: await getSession(context),
    },
  };
}

Instances of `Error: This Suspense boundary received an update before it finished hydrating`

I'm currently on v1.02 and recently tried to upgrade to v2.01. This went fine (and a ton of boilerplate was removed, well done!) but my automated tests started to get the error Error: This Suspense boundary received an update before it finished hydrating.

I didn't notice this error in development when using the site normally and I suspect that it might have something to do with how aggressively playwright controls the browser (i.e. it will navigate to another page very quickly after the first page has rendered some HTML).

I don't understand the internals of relay-nextjs very well but it it possible that something changed in the new version that might be related to suspense boundaries?

I found some details around how hard it is to debug where this occurring (facebook/react#25625) and some other users that have tracked this to a useEffect hook problem, however the stack trace is not very useful for figuring out exactly where it went wrong.

Just curious if anyone else has run into this!

React version 18.2.0
Next 13.3.0

preloaded query is null when wrapping a nested component using withRelay

Hi!
I'm struggling a bit with usePreloadedQuery in nested components.
If implemented in root component (e.g. a page component) it works (first snippet). On the other hand if implemented deeper in the tree (second snippet), it fails with error

Cannot read property 'fetchKey' of null

(This error comes from react-relay/lib/relay-hooks/usePreloadedQuery.js (40:33) because the preloadedQuery object passed to it is null.)

TL;DR;

  1. Is it a good practice to have preloaded queries in nested components?
  2. Is withRelay HOC appropriate to be used for nested components?

If you want to reproduce easily, you can use this repo example, edit pages/index.tsx like this:

// rest of the file...

// export default withRelay(FilmList, FilmListQuery, {
const MyNestedComponent = withRelay(FilmList, FilmListQuery, {
  createClientEnvironment: () => getClientEnvironment()!,
  createServerEnvironment: async () => {
    const { createServerEnvironment } = await import(
      'lib/server/relay_server_environment'
    );

    return createServerEnvironment();
  },
});

// Add this default export instead
export default function (props: any) {
  console.log('props', props);
  return <MyNestedComponent {...props} />;
}

Then run the server :
image

Environment
Node: 14
Next: 12
relay-nextjs: 0.7.0
react-relay: 13.1.1
react-compiler: 13.1.1

My implementation

Since this issue is related to my implementation, here are a few details, to follow on what I wrote above. (and also to help anyone coming from google that would be looking for snippets)

✅ Calling usePreloadedQuery in _app > Page Component (wired using withRelay)

const PAGE_QUERY = graphql`
  query all_items_Query {
    allItems {
      # this fragment is defined in AllItems component
      ...list_items
    }
  }
`;

function WrappedComponent({ preloadedQuery }: RelayProps<{}, all_items_Query>) {
  const query = usePreloadedQuery(PAGE_QUERY, preloadedQuery);
  return <AllItems relayRef={query.allItems} />;
}

// Page Component
export default withRelay(WrappedComponent, PAGE_QUERY, {
  createClientEnvironment: () => getClientEnvironment()!,
  createServerEnvironment: async (ctx) => {
    const { createServerEnvironment } = await import(
      "lib/server/relay_server_environment"
    );

    const session = await getSession(ctx);

    const token = session?.user.apiToken;

    return createServerEnvironment(token);
  },
});

❌ calling usePreloadedQuery in _app > Page Component > Another Component (wired using withRelay)

const PAGE_QUERY = graphql`
  query all_items_Query {
    allItems {
      # this fragment is defined in AllItems component
      ...list_items
    }
  }
`;

function WrappedComponent({ preloadedQuery }: RelayProps<{}, all_items_Query>) {
  const query = usePreloadedQuery(PAGE_QUERY, preloadedQuery);
  return <AllItems relayRef={query.allItems} />;
}

const WrappedComponentWithRelay = withRelay(WrappedComponent, PAGE_QUERY, {
  createClientEnvironment: () => getClientEnvironment()!,
  createServerEnvironment: async (ctx) => {
    const { createServerEnvironment } = await import(
      "lib/server/relay_server_environment"
    );

    const session = await getSession(ctx);

    const token = session?.user.apiToken;

    return createServerEnvironment(token);
  },
});


// Page component
export default function () {
  return <WrappedComponentWithRelay />;
}

_app and _document

My _app component is taken from the example in this repo. It implements the same behavior:

// src/pages/_app.tsx
const clientEnv = getClientEnvironment();
const initialPreloadedQuery = getInitialPreloadedQuery({
  createClientEnvironment: () => getClientEnvironment()!,
});

const MyApp: React.FC<MyAppProps> = ({
  Component,
  pageProps: { session, ...pageProps },
  emotionCache = clientSideEmotionCache,
}) => {
  const relayProps = getRelayProps(pageProps, initialPreloadedQuery);
  const env = relayProps.preloadedQuery?.environment ?? clientEnv!;

  return (
    <RelayEnvironmentProvider environment={env}>
      <Layout>
        <Component {...pageProps} {...relayProps} />
      </Layout>
    </RelayEnvironmentProvider>
  );
};

export default MyApp;
// src/pages/_document.tsx

interface DocumentProps {
  relayDocument: RelayDocument;
}

class MyDocument extends NextDocument<DocumentProps> {
  static async getInitialProps(ctx: DocumentContext) {
    const relayDocument = createRelayDocument();

    const renderPage = ctx.renderPage;
    ctx.renderPage = () =>
      renderPage({
        enhanceApp: (App) => relayDocument.enhance(App),
      });

    const initialProps = await NextDocument.getInitialProps(ctx);
    return {
      ...initialProps,
      relayDocument,
    };
  }

  render() {
    const { relayDocument } = this.props;

    return (
      <Html>
        <Head>
          <relayDocument.Script />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

Authenticated request client side

Hi! Awesome library you have here.

Wondering if by any chance you have a nice way of doing authenticated requests on client-side.

Any chance we could expose ctx from next for createClientEnvironment?
Would this be a good idea?

EDIT: Forked the package, the code below will not work.
May I have other ideas how to have make authenticated requests client-side via relay-nextjs using withRelay?

It would look like this for getClientEnvironment

// we receive `ctx` here to determine authenticated headers
export function createClientNetwork(ctx) {
  return Network.create(async (params, variables) => {
    const response = await fetch('/api/graphql', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        // we set the auth header here
        headers['x-auth'] = ctx.token
      },
      body: JSON.stringify({
        query: params.text,
        variables,
      }),
    });

    const json = await response.text();
    return JSON.parse(json, withHydrateDatetime);
  });
}

let clientEnv: Environment | undefined;
// this is where we will have access for the `ctx`
export function getClientEnvironment(ctx) {
  if (typeof window === 'undefined') return null;

  if (clientEnv == null) {
    clientEnv = new Environment({
     // pass ctx to createClientNetwork
      network: createClientNetwork(ctx),
      store: new Store(new RecordSource(getRelaySerializedState()?.records)),
      isServer: false,
    });
  }

  return clientEnv;
}

Handing GraphQL server errors

First of all thanks for this great package, super helpful ❤️

I'm trying to implement a redirect to login when my GraphQL server responds with a 401. One of the examples in this repo handles this by checking for valid auth before the GraphQL request which works but is not ideal given I'd need to make an auth check on every request.

It sounds like the best way to redirect on 401 would be to throw in the relay network/env and somehow catch it somewhere in getServerSideProps, so that we're able to return a nextjs redirect.

Unfortunately I don't think it's possible to handle this logic in userland through the getServerSideProps option this package provides, because that happens before loadQuery is called. https://github.com/RevereCRE/relay-nextjs/blob/main/src/wired/component.tsx#L81-L116

Is there a better way to do this? Happy to contribute something if needed 🙇

Remove default fallback on page load?

I noticed that even if I don't specify a fallback as a config parameter for withRelay, there's still a default fallback that is showing which is causing a brief flash when I navigate between pages.

I noticed this also on the example in the repo.

Is there a config parameter or recommendation to avoid this?

"Expected preloadedQuery to not be disposed yet." error

First of all, thank you for the library 🙏

For one of my component, I'm using the useQueryLoader + usePreloadedQuery hooks.

Everything was working flawlessly until I updated from React 17.0.2 to 18.1.0. I started getting:

next-dev.js?3515:25 Warning: usePreloadedQuery(): Expected preloadedQuery to not be disposed yet. This is because disposing the query marks it for future garbage collection, and as such query results may no longer be present in the Relay store. In the future, this will become a hard error. 
    at eval (webpack-internal:///./components/index/UserBeats.tsx:22:26)
    at LoadableImpl (webpack-internal:///./node_modules/next/dist/shared/lib/loadable.js:101:38)
    at Suspense
    at eval (webpack-internal:///./components/index/IndexLoggedIn.tsx:120:69)
    at LoadableImpl (webpack-internal:///./node_modules/next/dist/shared/lib/loadable.js:101:38)
    at Suspense
    at eval (webpack-internal:///./pages/index.tsx:45:73)
    at div
    at div
    at eval (webpack-internal:///./components/nav/NavWeb.tsx:50:27)
    at RelayEnvironmentProvider (webpack-internal:///./node_modules/react-relay/lib/relay-hooks/RelayEnvironmentProvider.js:21:24)
    at FirebaseContextProvider (webpack-internal:///./lib/firebase.tsx:125:29)
    at MyApp (webpack-internal:///./pages/_app.tsx:112:27)
    at ErrorBoundary (webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/client.js:8:20746)
    at ReactDevOverlay (webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/client.js:8:23395)
    at Container (webpack-internal:///./node_modules/next/dist/client/index.js:323:9)
    at AppContainer (webpack-internal:///./node_modules/next/dist/client/index.js:825:26)
    at Root (webpack-internal:///./node_modules/next/dist/client/index.js:949:27)

It's not crashing or anything, it's just a warning and it actually disappears when going to prod, but I was wondering if it would ring a bell to you. I really cannot find anything online :/

Redirect post query on relay-nextjs v2

Hi everyone,

I'm migrating from relay-nextjs v1 to v2, following the example migration. Everything seems to be working as expected and the new set up process is much simpler.

However, in my app there are some pages where the user is conditionally redirected based on data - that is, depending on some user info in the database, they are redirected to another page. Previously, we used serverSidePostQuery to do this:

export default withRelay(Report, ReportQuery, {
  serverSidePostQuery: (result, context) => {
    if (!viewerCanSee(result.data)) {
      context.res?.writeHead(302, { Location: "/dashboard" });
      context.res?.end();
    }
  },
});

With the upgrade to v2, this callback has been removed. I'd like to understand more about what the recommended strategy is for this use case. I'm also interested in contributing if necessary.

Thanks for your help.

Wired components getServerInitialProps error when creating operation descriptor

Hiya! First, thank you for all the work done on this lib - it has saved so much time and is a pleasure to use.

I'm investigating an issue in our codebase that has popped up, and while I'm not sure the root cause I've tracked it down to this line here. When I log what query is, it is of type ConcreteRequest, not { default: ConcreteRequest } like the code assumes. This obviously leads to issues since accessing .default on ConcreteRequest is undefined. I am very confused as there isn't anything I can see which could have changed this behavior and previously, this has worked fine.

Would be very happy to PR a quick fix to check for the existence of default, but it's probably a better idea to figure out why this is happening/what as changed. Maybe a minor patch somewhere which gets automatically upgraded as this library is using non-obvious APIs in some cases?

As a sanity check, when I change the compiled js from const operationDescriptor = relay_runtime_1.createOperationDescriptor(query.default, variables); to const operationDescriptor = relay_runtime_1.createOperationDescriptor(query, variables); everything works fine.

withRelay fallback not working

I'm passing a fallback component to the withRelay function but I still get the fallback in _app.tsx when the page suspends

Here's my function:

export function withCustomRelay<Props extends WiredProps, ServerSideProps>(
  Component: ComponentType<Props>,
  query: GraphQLTaggedNode,
  options?: Partial<WiredOptions<Props, ServerSideProps>>
) {
  return withRelay(Component, query, {
    fallback: <section>Custom fallback</section>,
    createClientEnvironment: () => getClientEnvironment(),
    createServerEnvironment: async (ctx) => {
      const { createServerEnvironment } = await import(
        './createServerEnvironment'
      );
      return createServerEnvironment({ request: ctx.req });
    },
    ...(options || {}),
  });
}

Idk If I'm missing some config or what could it be

Thanks!

Question around seemingly optional `<Suspense />` usage

Hello RevereCRE, congratulations on the 2.0.0 release!

I've been adopting relay-nextjs where I work and it's pretty unbelievable how much of an improvement the 2.x release is in terms of boilerplate reduction.

In my testing, I've found that in the component there appears to be an optional suspense boundary. And I'm wondering if either I'm missing something, or if this can optionally be entirely removed.

I've been patching relay-nextjs and removing said Suspense boundary and it can create the illusion that the layout is "shared" across navigations..

The default behavior with the Suspense boundary:

Screen.Recording.2023-05-23.at.3.56.21.PM.mov

With a relay-nextjs patch, removing the Suspense boundary:

Screen.Recording.2023-05-23.at.3.51.37.PM.mov

And finally, the patch itself

relay-nextjs+2.0.1.patch

Granular Loading Indicator

Hi,

Thanks for this great library.

I need more granular control for displaying loading state. I want to display a loading spinner in a child component, not the whole page.

I commented <Suspense> from component.tsx, but I get a weird error when navigating.

This is the error that was printed in my browser console for "example project":

image

And this is displayed in page:

image

The error says "above error", but there is no error printed above.

How set headers from server request?

Hello, tell me how to set the headers received from the server on the client?

In apollo, this is done via setContext is there anything good here.

I am trying to implement adding to cart for unauthorized users. For this to work, I need to always update the user session, since it comes from the server.

I also noticed that when the page is redirected, usePreloadedQuery is triggered twice, is this normal?

It is also interesting how to implement work with the jwt token, in particular, tracking the error and performing a re-request.

Crashes with serverless functions

Hey all,

Great work with this library! It's been helpful on my Relay + Next journey.

I have a GraphQL server that is currently running on a Serverless Function, but I noticed that it returns a 413 Error when the payload is bigger than 250kb.

From my research, serverless functions seem to have a limit of 250kb for Asynchronous invocation and 6MB for Synchronous invocation (see here), so that could be happening because of the way that the fetch call is executed. So I experimented with fetching directly from my GraphQL server in getServerInitialProps, and it did work.

I am currently suspecting it can be related to #70 and the way loadQuery works. I tried making the getClientInitialProps async and calling the ensureQueryFlushed, but no success so far.

Any advice is appreciated and I'd be happy to work on a PR that addresses this if it's interesting for anyone else.

Usage with CSP

Have to start by thanking for this great library!

I'm wondering why this injects script tag in the head instead of putting the data inside __NEXT_DATA__? Only stumbled on this because our CSP blocks the inline script meaning that hydration fails.

Would you accept PR for one of the following:

  • Put state inside __NEXT_DATA__ instead of custom script tag
  • Use type="application/json" script tag instead, so it doesn't get blocked by CSP
  • Add optional nonce prop to WiredDocument.Script to make it possible to use safe CSP with this. Assuming that there's some reason for the current choices, this would likely be the best approach.

Example is broken

Overview

In my environment, example throw following error.

Server Error
TypeError: _interopRequireDefault is not a function

This error happened while generating the page. Any console logs will be displayed in the terminal window.

In calls tack,

node_modules/relay-runtime/lib/store/RelayStoreUtils.js (14:27)

Reproduce code

git clone [email protected]:RevereCRE/relay-nextjs.git
cd elay-nextjs
yarn
cd relay-nextjs && yarn build -w
cd example && yarn dev

My Environment

yarn version v1.22.19
node version v16.17.1
relay-nextjs v1.0.2

I downgraded to v1.0.0 and it worked but v1.0.2 is broken.

Authenticated requests from the client

Hi Ryan,

I am not sure if I could ask questions in this github issue but please me know if there is a better place to ask questions.

Could you shed some light on this?

I would like to make authenticated requests to a graphql api, which means I need to include a Authorization header with a token string value.

By following the example in the doc, I am able to pass the auth token to createServerEnvironment.
This works fine for the initial landing page (e.g. Page A), since the request was made on the server side which uses the server env (which uses auth token).

However, if I naviagte to page B and back to page A (or page C which also makes graphql requests), it will defintely fail since the request is made from the client, and I didn't create a client env with auth token

But how can I create a client environment with a auth token? I am using next-auth, two ways to get session tokens are useSession and getSession. None of them will work if I call these inside getClientEnvironment since useSession needs to be wrapped with a SessionProvider, and getSession is a async function.

Another question: Do I have to to create a relay client environemnt with auth token? In other words, can I make all requests are made from the server side (which uses relay server environemnt), so that all pages are hydrated before it returns to the client?

Thanks in advance!

add example

it would make it easy to use if there is a full runnable example

tks for this package, it is awesome

Implementation with getServerSideProps

Hi 👋

Thanks for this library -- its great. I have a question regarding data fetching. I see that currently getInitialProps is used. What is the rationale behind this?

If it would be possible to move to getServerSideProps, we could get rid of the fallback + we would be able to tree shake server code.

Shallow route change refetches data

When replacing route with something using shallow option, it seems like relay-nextjs still lets relay refetch the data. I'm not sure if that's intended as as far as I know, the only reason to use shallow routing would be to not trigger data fetching again. 🤔

The usecase is to avoid refetching all of the data when you use filters in our site. We store these filter options in the url (using shallow replace), but it still triggers relay to refetch all of the data.

Relevant issues, but I made a new issue since the old ones don't really explain this particular issue: #47 and #45

Multiple queries on one page

What problem does this feature proposal attempt to solve?

My pages are generated using several GraphQL queries. Some of them can be cached and reused. But relay-nextjs allows only one query to be preloaded.

Which possible solutions should be considered?

I have implemented a proof of concept in https://github.com/k0ka/relay-nextjs/tree/multi-query
But it is not backward compatible. The main idea is that instead of

WiredProps<P extends {},Q extends OperationType> 

it is using

WiredProps<P extends {},Q extends {[key: string]: OperationType}>

So the user may pass some hash of queries to preload them. The example page would look like this:

function UserProfile({ preloadedQuery }: RelayProps<{}, {preloadedQuery: profile_ProfileQuery}>) {

And one can preload several queries like this:

function UserProfile({ query1, query2 }: RelayProps<{}, {query1: profile_ProfileQuery, query2: profile_AnotherQuery}>) {

Why backward compatibility breaks?

While it looks easy to add the backward compatibility on the first glance, but after looking in-depth, there are a lot of nuances. And it might end up being a big kludge.

For example, you assume the page always has the preloadedQuery prop to extract the server environment from it:

  const relayProps = getRelayProps(pageProps, initialPreloadedQuery);
  const env = relayProps.preloadedQuery?.environment ?? clientEnv!;

We can enforce that this prop is always set up for some query (it looks like a kludge) or change it as I do in my PoC (breaking BC)

    const [relayProps, serverEnv] = getRelayProps(pageProps, initialPreloadedQueries);
    const env = serverEnv ?? clientEnv!;

Also it is not easy to distinguish OperationType and [key: string]: OperationType in javascript as they both might be objects. We have to check some of the OperationType key type that's always defined and is not an object.

What's next?

I implemented the PoC in a more elegant but BC Breaking way. The next question is whether you really need this feature in the package. It might be too complex and specific for my case. In this case I'll just maintain it in my repo.

If you do need it, we have to decide on the way how to implement it.

This is a bit tricky for two reasons:

This is a bit tricky for two reasons:

  1. I'm not sure having the Relay environment change network after initializing works?

  2. Requests must be made on both the client side and server side, so using local storage based authentication won't work because it's not available on the server.

For these reasons I recommend using cookies for your auth solution.

Originally posted by @rrdelaney in #20 (comment)

About SSG support

Question

Does it support ssg (getStaticPaths/getStaticProps)? If so, can you please include it in the README or samples?


Thanks to your abstraction of server-side and client-side store sharing and hydration, it will free us from the hassle of configuration. It's a very exciting project.

Page component got mount again when using array in query variables

latch.current = !Object.keys(queryVariables as {}).every(
(key) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(queryVariables as any)[key] === (initialQueryVars.current as any)[key]
);

Seems it's because relay-nextjs is using shallow comparison so if we're using array in the qurey variables, the useHaveQueryVariablesChanges will return true by each of the re-render

Is that possible to use deep compare?

Local cache is not used for "page level" queries

Somewhat relevant: #41 (comment)

I tried to work around the shallow routing issue by just not using shallow routing. But I've ran into an issue where cache is not used at all when the page level query is ran. It's reproducible in the example app as well, even if you update relay-nextjs to 0.6.0.

TypeError: can't define property "__indexColor": Object is not extensible

Trying to use Next relay data to feed to my react-force-graph2d following Will's tutorial https://lyonwj.com/blog/graph-visualization-with-graphql-react-force-graph. And no errors with static data in the page but log " TypeError: can't define property "__indexColor": Object is not extensible " in the browser.

'use client';
import { withRelay } from 'relay-nextjs';
import { getClientEnvironment } from '../lib/client-environment';
import { RelayProps, graphql, usePreloadedQuery } from "react-relay";
import node, { pages_GetNodesQuery as pages_GetNodesQueryType } from "../queries/__generated__/pages_GetNodesQuery.graphql";
import Spinner from 'react-bootstrap/Spinner';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import UserProfile from 'components/user-profile';
import dynamic from 'next/dynamic';
import { useState } from 'react';

const ForceGraph2D = dynamic(() => import('../lib/force.module'), {
  ssr: false
});

const pages_GetNodesQuery = graphql`
  query pages_GetNodesQuery {
    nodes {
    id
    firstName
    lastName
  }
  links{
    id
    source
    target
    parent 
  }
}
`;

function HomePage({ preloadedQuery }: RelayProps<{}, pages_GetNodesQueryType>) {
  const data = usePreloadedQuery(pages_GetNodesQuery, preloadedQuery);


  return (
    <>
      <Container>
        <Row>
          <Col md={9}>
            <ForceGraph2D graphData={data} />;
          </Col>
          <Col md={3}>
            <UserProfile />
          </Col>
        </Row>
      </Container>
    </>
  );
}

function Loading() {
  return (
    <Spinner animation="border" variant="primary" role="status">
      <span className="visually-hidden">Loading...</span>
    </Spinner>
  );
}


export default withRelay(HomePage, pages_GetNodesQuery, {
  // Fallback to render while the page is loading.
  // This property is optional.
  fallback: <Loading />,
  // Create a Relay environment on the client-side.
  // Note: This function must always return the same value.
  createClientEnvironment: () => getClientEnvironment()!,
  /* serverSideProps: async (ctx) => {
    // This is an example of getting an auth token from the request context.
    // If you don't need to authenticate users this can be removed and return an
    // empty object instead.
    const { getTokenFromCtx } = await import('lib/server/auth');
    const token = await getTokenFromCtx(ctx);
    if (token == null) {
      return {
        redirect: { destination: '/login', permanent: false },
      };
    }
 
    return { token };
  }, */
  // Server-side props can be accessed as the second argument
  // to this function.
  createServerEnvironment: async (
    ctx,
    // The object returned from serverSideProps. If you don't need a token
    // you can remove this argument.
    // { token }: { token: string }
  ) => {
    const { createServerEnvironment } = await import('../lib/server-environment');
    return createServerEnvironment();
  },
});

Help highly appreciated

WiredComponent.getInitialProps -> Always async

Hey there,

We noticed that WiredComponent.getInitialProps is sync on client and async on server. We would be interested in them being async on either or, since it's technically allowed in Next.JS

If this works for y'all, I'm happy to put out a PR.

Thanks!

next@12?

Hi,

first of all thank you for open sourcing your hard work to integrate react relay into next.js.

I stumbled upon this library when running into trouble integrating the great concepts and implementation of react-relay with the framework capabilities of next.js.

That being said I am starting a project on version 12 of next.js and was wondering if this library has been tested against it?

If not, are there any plans to do so?

Thanks!

Allow disabling of `Suspense` rendering

Related to #41 , I have a use case where my page's query will be refetched frequently (I have a filterable table that updated the query string when filters are applied). Rather than having a static fallback for the Suspense component, I want to keep rendering the previous page component's (with its props, and maybe an extra queryIsLoading prop) while the new query is loading.
We've done that in a previous project (using Relay 10 and the QueryRenderer) and it worked well.

I don't think this is feasible right now, and I think that the simplest way to allow this is to add a renderSuspense boolean option to the withRelay HOC (defaults to true). When false, users of the package would have to render their own Suspense in _app (which I think would allow me to do what I described above).

It's not at the top of my list right now, but I'll look into this in a few weeks and I can contribute to a PR (unless someone else gets to it first 😅 )

How to use `variablesFromContext`?

Hi Ryan, thank you so much for the work on this package!

After seraching in the issues, digging into the code and many trials and errors, I couldn't get the following working.

Use case: I'd like to supply user_id in varaibles of a graphql query after user is logged in, possibly available on every page. The user_id is stored in a jwt token. I am using next-auth to create the token, store it in cookie etc.

Things I tried:

  1. Since I have the following serverSideProps defined, I found when debugging, I was able to access user_id from pageProps: ctx.components[ctx.route].props.pageProps?.user_id at some point. Then I tried the following:
  serverSideProps: async (ctx) => {
          const { getToken } = await import("next-auth/jwt");
          const token = await getToken({
              req: ctx.req,
              secret: process.env.SECRET,
          });
   
          return {
              ...,
              userId: token?.user_id,
          };
      },
   
  variablesFromContext: async (ctx) => {
      return {
          user_id:  ctx.components[ctx.route].props.pageProps?.user_id,
      };
  },

The page is able to load but the console will print out errors everytime I refresh the page:

error - src/pages/index.tsx (94:36) @ variablesFromContext
TypeError: Cannot read properties of undefined (reading 'undefined')
  92 | 
  93 |         return {
> 94 |             user_id: ctx.components[ctx.route].props.pageProps?.user_id,
     |                                    ^
  95 |         };
  96 |     },
  97 |     createClientEnvironment: () => getClientEnvironment()!,

This runtime error shows every time I visit this page from another page.

  1. Another thing I tried is to directly call getToken from next-auth
    variablesFromContext: async (ctx) => {
        const { getToken } = await import("next-auth/jwt");
        const token = await getToken({
            req: ctx.req,
            secret: process.env.SECRET,
        });
 
        return {
            user_id: token.user_id
        };
    },

This will have runtime error:

Unhandled Runtime Error
Error: Must pass `req` to JWT getToken()

  69 | }
  70 | const { getToken } = await import("next-auth/jwt");
> 71 | const token = await getToken({
     |                    ^
  72 |     req: ctx.req,
  73 |     secret:  process.env.SECRET,
  74 | });

Not sure if it's relevant but my IDE typescript plugin tells me ctx passed in variablesFromContext has type NextPageContext | NextRouter, which I suppose NextRouter won't work here.

Appreciate any help!

How to disable SSR?

It is really hard to debug when something goes wrong server-side.

How do I force relay-nextjs to serve static JavaScript and make GraphQL queries client-side?

`WiredComponent` should always render a `Suspense` on the client

Not sure if I'll have time to make a small example, but I observed the following behaviour:

  • When rendering a page for the first time on the client, the CSN prop is false (meaning it won't be rendered inside a Suspense component). Not sure if its an error from my code or if that's always the case
  • Now there are two scenarios after this:
    • If we change the page with some client-side routing, CSN is back to true, and all is well again.
    • If the query variables changes, and we stay on the same page (i.e. by having an action that changes the query string), then usePreloadedQuery will throw a promise at some point, but there is no Suspense to catch it, so it causes an error and the page fails to render.

I've only tested this with a development build so far, so maybe it's a different behaviour in production.

Shallow routing support

This has been discussed in #41 but I think I will need shallow routing even though you said it's not supported. Not really asking you to make it, I'm willing to make a PR if you think it's ok idea and I can come up with some acceptable solution. Right now I'm thinking of just making option for always rendering suspense (even server side), since React 18 supports server-side suspense. Not sure if that would solve all the issues with shallow routing.

The usecase is following:
We have infinite feed in our page and want to be able to return to the same position when user visits other page. Obvious solution to this is storing the necessary info in the url, but we can't really do that properly without shallow routing. If we just put the starting cursor + count and let relay fetch the whole query again, it'll mean that we will constantly refetch all of the content in the feed that has already been loaded instead of just the next x items.

Issue with react-i18next / next-i18next

I am trying to use withRelay on a page that need preloading of info (classic /account/edit where I want fields to be filled with user data upfront).
So I started converting a simple page by adding withRelay required logic.

I started with this (working, but no query to prefill)

import page from "../../src/pages/PageAccountEdit.bs.js";
import { node as pageQuery } from "../../src/__generated__/PageAccountEdit_getUser_Query_graphql.bs.js";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import { getSession } from "next-auth/react";

export default page;
export async function getServerSideProps(context) {
  const session = await getSession(context);
  if (session === null) {
    return {
      redirect: {
        destination: "/auth/login?next=" + encodeURIComponent(context.pathname),
        permanent: false,
      },
    };
  }

  return {
    props: {
      session,
      ...(await serverSideTranslations(context.locale, ["common"])),
    },
  };
}

to this (same page, with a new query and withRelay call)

import page from "../../src/pages/PageAccountEdit.bs.js";
import { node as pageQuery } from "../../src/__generated__/PageAccountEdit_getUser_Query_graphql.bs.js";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import { getSession } from "next-auth/react";

async function getServerSideProps(context) {
  const session = await getSession(context);
  if (session === null) {
    return {
      redirect: {
        destination: "/auth/login?next=" + encodeURIComponent(context.pathname),
        permanent: false,
      },
    };
  }

  return {
    props: {
      session,
      ...(await serverSideTranslations(context.locale, ["common"])),
    },
  };
}

import { withRelay } from "relay-nextjs";
import * as RelayEnvClient from "../../src/RelayEnvClient.bs";
import * as RelayEnvServer from "../../src/RelayEnvServer.bs";

export default withRelay(page, pageQuery, {
  createClientEnvironment: () => RelayEnvClient.getClientEnvironment(),
  serverSideProps: getServerSideProps,
  createServerEnvironment: async (ctx, serverSideProps) => {
    return RelayEnvServer.createServerEnvironment(
      serverSideProps.props.session
    );
  },
});

I am getting an warning in the browser

react-i18next:: You will need to pass in an i18next instance by using initReactI18next

And the translations are not available.
What is weird is that my getServerSideProps is exactly the same, export serverSideTranslations properly.
Any idea why withRelay might conflict somehow with next-i18next ?

update installation docs

hi, thanks for the repository and docs. Also, I see some improvements to aggregate to the amazing job.

I guess I will have a free time into the next days and I can send a pull request doing these items below. Until there if someone can feel free to do it:

  • add the same topics from npm to yarn
  • upgrade webpack section to the actual one from example
    update from
module.exports = {
  webpack: (config, { isServer, webpack }) => {
    if (!isServer) {
      // Ensures no server modules are included on the client.
      config.plugins.push(new webpack.IgnorePlugin(/lib\/server/));
    }

    return config;
  },
};

to https://github.com/RevereCRE/relay-nextjs/blob/main/example/next.config.js

module.exports = {
  webpack: (config, { isServer, webpack }) => {
    if (!isServer) {
      // Ensures no server modules are included on the client.
      config.plugins.push(new webpack.IgnorePlugin(/lib\/server/));
    }

    return config;
  },
};

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.