DEV Community

Marais Rossouw
Marais Rossouw

Posted on

Relay and SSR using createOperationDescriptor

tldr;
Use createOperationDescriptor, and ask RelayEnvironment for the data, rather than sending queryProps to a window object.

const Component = () => {
    const environment = getRelayEnvironment(records);
    const queryConcreteRequest = getRequest(someGraphQLQuery);

    return (<SomeQueryAskingComponent {...environment.lookup(
        createOperationDescriptor(
            queryConcreteRequest,
            someVariables,
        ).fragment,
    ).data}/>);
};
Enter fullscreen mode Exit fullscreen mode

A lot of what I see online in terms of articles around Relay + SSR is that they all follow a pattern similar to this:

  • They have a page level query ✅
  • The page level query gets fetched on the server with a fetchQuery
  • They then dump the store to a window object (for hydration) ✅

But where they all fall apart is when they also flush the queryProps to the window object that you'd typically give to the component. ❌

Before I tell you why that is wrong, lets look at the flow of how things work.

Relay has 2 parts really, you have a RelayEnvironment, and a query (fragments, query renderes, etc..). The environment lives in a RelayEnvironmentProvider so when you have a useFragment or createFragementContainer it creates an identifier, and resolves data from its props. Those props typically come from a queryRenderer's render prop, or in SSR world come directly from a fetchQuery.

So the reason why the above is wrong is because if you have a massive page level query. The store is flushed to the window object, as well as the query props! Both contain duplicated bits of data. One being the map of ID's, and one being the "resolved data" for your query.

Now, in a production app using Next.js as the example, there is a NEXT_DATA, which is basically the window object constructed as a way for server side to relay (pun intended) initial props to the client side, for hydration. Now if you flushed both you end up with massive payloads. As an example, I had a blog that asked for authors, article bodies, tags, comments, reviews, related article's etc... All of that rolled into something like 46k lines of json (please just accept that it was large), which is horrible right!

Now lets get to the point of the article - How to fix this!

Instead of flushing the queryProps in the NEXT_DATA. Just figure out a way to resolve the queryProps on the client using nothing but the store. It's simple really.

You need 2 things: a reference to the query itself, and a RelayEnvironment. The query forms something of an "id" into the store, and the environment has the store. So create a relay store identifier through a createOperationDescriptor, which takes the query and its variables and spits out an id. Then use that id to lookup the data in the store, and simply give that to the component. Happy days! And now you're left with, and from our example, an almost 1k line JSON. Some ridiculous savings there!

Here is an example of this:

import { fetchQuery, graphql } from 'react-relay';
import { createOperationDescriptor, getRequest } from 'relay-runtime';

const WithData = (PageComponent, options) => {

    const WrappedComponent = ({ variables, records }) => {
        const environment = getRelayEnvironment(records);
        const queryConcreteRequest = getRequest(options.query);
        const requestIdentifier = createOperationDescriptor(
            queryConcreteRequest,
            variables,
        );

        const pageData = environment.lookup(
            requestIdentifier.fragment,
        );

        return <RelayEnvironmentProvider environment={environment}>
            <PageComponent {...pageData.data}/>
        </RelayEnvironmentProvider>;
    };

    WrappedComponent.getInitialProps = async () => {
        const environment = getRelayEnvironment();

        const variables = options.variables();

        await fetchQuery(
            environment,
            options.query,
            variables,
        );

        const records = environment
            .getStore()
            .getSource()
            .toJSON();

        return {
            variables,
            records,
        };
    };

    return WrappedComponent;
};

export default withData(
    ({ article }) => <h1>{article.name}</h1>,
    {
        query: graphql`
                    query ArticleQuery($slug: String!) {
                        article(slug: $slug) {
                            name
                        }
                }`,
        variables() {
            return { slug: 'example' };
        },
    });


Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
augustocalaca profile image
Augusto Calaca

does your getRelayEnvironment look like this: createRelayEnvironment?

Collapse
 
marais profile image
Marais Rossouw • Edited

This is an adaptation of my getRelayEnvironment function: gist.github.com/maraisr/32a7b0c1a4....

I'd like to change the fetch function though to return an Observable though, so that it can abort fetch's when components un-mount.

Outside that; the withData HoC outlined in my article is still very much the case for us.

BUT! I will be updating this article in the next coming month to utilize the new preloadQuery and usePreloadedQuery methods along with EntryPointContainer from Relay.

Collapse
 
famence profile image
Andrey

Thanks a lot!