Skip to content

Instantly share code, notes, and snippets.

@AndrewIngram
Last active October 23, 2022 01:56
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AndrewIngram/a77afd0c2d7a7bba52913d7a333d88bf to your computer and use it in GitHub Desktop.
Save AndrewIngram/a77afd0c2d7a7bba52913d7a333d88bf to your computer and use it in GitHub Desktop.
Updated Next.js with Relay (still using getInitialProps)
import React, { useRef } from "react";
import { ReactRelayContext } from "react-relay";
import { default as NextApp } from "next/app";
import createRelayEnvironment from "../relay/createRelayEnvironment";
export default function App({ Component, pageProps }) {
const environmentRef = useRef(
createRelayEnvironment({
records: pageProps && pageProps.queryRecords,
request: null,
})
);
return (
<ReactRelayContext.Provider
value={{
environment: environmentRef.current,
}}
>
<Component {...pageProps} />
</ReactRelayContext.Provider>
);
}
import React from "react";
import { fetchQuery } from "react-relay";
import { useQuery } from "relay-hooks"; // this it to make it simpler than using a QueryRenderer, also gives us store-only
import createRelayEnvironment from "./createRelayEnvironment";
export default function (ComposedComponent, options = {}) {
const Container = (pageProps) => {
const { props } = useQuery(options.query, pageProps.queryVariables, {
fetchPolicy: "store-only",
});
return <ComposedComponent {...pageProps} {...props} />;
};
Container.displayName = `PageContainer(${ComposedComponent.displayName})`;
Container.getInitialProps = async (ctx) => {
const environment = createRelayEnvironment({
request: ctx.req || null,
});
// Evaluate the composed component's getInitialProps()
let composedInitialProps = {};
if (ComposedComponent.getInitialProps) {
composedInitialProps = await ComposedComponent.getInitialProps(ctx);
}
let queryProps = {};
let queryRecords = {};
let variables = {};
let pageHeaders;
if (options.query) {
variables = options.getVariables ? options.getVariables(ctx) : {};
queryProps = await fetchQuery(environment, options.query, variables);
queryRecords = environment.getStore().getSource().toJSON();
}
const props = {
...composedInitialProps,
queryVariables: variables,
queryRecords,
};
if (options.getHeaders) {
const headers = options.getHeaders({ ...props, ...queryProps });
if (headers) {
pageHeaders = headers;
}
}
if (ctx.res) {
// you can skip this if you don't want to mess with cache headers for you page
ctx.res.setHeader(
"Cache-Control",
"s-maxage=1, stale-while-revalidate=59"
);
return { ...props, pageHeaders };
}
return {
queryVariables: variables,
};
};
return Container;
}
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import fetch from "isomorphic-unfetch";
let relayEnvironment = null;
function getHeadersFactory(request) {
return () => {
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
return headers;
};
}
function getFetchQuery(apiUrl, getHeaders) {
// I'm using persisted queries which is why I have operation.id here, you'll want to tweak this method if you're not
function fetchQuery(operation, variables, cacheConfig, uploadables) {
return fetch(apiUrl, {
method: "POST",
headers: getHeaders(),
body: JSON.stringify({
documentId: operation.id,
variables,
}),
}).then(async (response) => response.json());
}
return fetchQuery;
}
export default function createRelayEnviroment({
records = {},
request = null,
} = {}) {
// Constructing your API URL is likely to be different to me, so tweak as needed.
const baseUrl = request
? `${request.headers["x-forwarded-proto"] || "http"}://${
request.headers.host
}`
: "";
const apiUrl = `${baseUrl}/api/graphql`;
// Create a network layer from the fetch function
const network = Network.create(
getFetchQuery(apiUrl, getHeadersFactory(request))
);
const store = new Store(new RecordSource(records));
if (!process.browser) {
return new Environment({
network,
store,
});
}
// reuse Relay environment on client-side
if (!relayEnvironment) {
relayEnvironment = new Environment({
network,
store,
});
}
return relayEnvironment;
}
import React from "react";
import { graphql } from "react-relay";
import createPageContainer from "../relay/createPageContainer";
import PostLayout from "../components/PostLayout";
import Header from "../components/Header";
import Shell from "../layouts/Shell";
function PostDetail({ config, post }) {
return (
<Shell
title={post.title}
>
<Header />
<PostLayout config={config} post={post} />
</Shell>
);
}
export default createPageContainer(PostDetail, {
query: graphql`
query PostDetailQuery($slug: String!) {
config {
...PostLayout_config
}
post: postBySlug(slug: $slug) {
title
slug
...PostLayout_post
}
}
`,
getVariables: ({ query: { slug } }) => ({ slug }),
});
@fi0
Copy link

fi0 commented Dec 17, 2020

Is slug a variable in the dynamic route?

@AndrewIngram
Copy link
Author

yeah

@quant-daddy
Copy link

Hi! It seems like it breaks for relay runtime v11. I've updated it and it seems to work. My only concern is I'm using toPromise which relay documentation asks explicitly to avoid (https://relay.dev/docs/api-reference/fetch-query/#behavior-with-topromise)

export function createPageContainer<T extends OperationType>(
  ComposedComponent: NextPage<{
    renderProps: RenderProps<T>;
    queryVariables: T['variables'];
  }>,
  options: {
    query: GraphQLTaggedNode;
    getVariables: (ctx: NextPageContext) => T['variables'];
  },
) {
  const Container = (
    pageProps: { queryVariables: Variables } & {
      children?: ReactNode;
      // data: T['response'];
    },
  ) => {
    const res = useQuery<T>(options.query, pageProps.queryVariables, {
      fetchPolicy: 'store-only',
    });
    return (
      <ComposedComponent
        {...pageProps}
        renderProps={res}
        queryVariables={pageProps.queryVariables}
      />
    );
  };

  Container.displayName = `PageContainer(${ComposedComponent.displayName})`;

  Container.getInitialProps = async (ctx: NextPageContext) => {
    // const environment = createRelayEnvironment({
    //   request: ctx.req || null,
    // });

    // Evaluate the composed component's getInitialProps()
    let composedInitialProps = {};
    if (ComposedComponent.getInitialProps) {
      composedInitialProps = await ComposedComponent.getInitialProps(ctx);
    }
    // let queryProps = {};
    // const queryRecords = {};
    let variables = {};
    // let pageHeaders;
    if (options.query) {
      variables = options.getVariables ? options.getVariables(ctx) : {};
      const r = await fetchQuery(ctx.environment, options.query, variables, {
        fetchPolicy: 'store-or-network',
        networkCacheConfig: {
          force: false,
        },
      }).toPromise();
      // console.log(
      //   `fetched records`,
      //   ctx.environment
      //     .getStore()
      //     .getSource()
      //     .toJSON(),
      // );
      // queryRecords = ctx.environment.getStore().getSource().toJSON();
    }

    // console.log(queryProps);

    const props = {
      ...composedInitialProps,
      queryVariables: variables,
      // queryRecords,
    };
    // if (options.getHeaders) {
    //   const headers = options.getHeaders({ ...props, ...queryProps });
    //   if (headers) {
    //     pageHeaders = headers;
    //   }
    // }

    if (ctx.res) {
      // // you can skip this if you don't want to mess with cache headers for you page
      // ctx.res.setHeader(
      //   'Cache-Control',
      //   's-maxage=1, stale-while-revalidate=59',
      // );
      return { ...props };
    }
    return {
      queryVariables: variables,
    };
  };

  return Container;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment