react-relay-network-modern-ssr icon indicating copy to clipboard operation
react-relay-network-modern-ssr copied to clipboard

Components with createFragmentContainer not resolving fields

Open juhaelee opened this issue 5 years ago • 7 comments

When I have a component with a createFragmentContainer HoC, on the server side I get passed props to that component that look similar to this:

{ id: 'Qm9vay0x',
  __fragments: { User_viewer: {} },
  __id: 'Qm9vay0x',
  __fragmentOwner:
   { fragment: { dataID: 'client:root', node: [Object], variables: {} },
     node:
      { kind: 'Request',
        fragment: [Object],
        operation: [Object],
        params: [Object],
        hash: '7f3a8b6ed33bc16971bcdc4704b94b0e' },
     root: { dataID: 'client:root', node: [Object], variables: {} },
     variables: {} }
}

When I want the props to look something like this (which is what the client gets):

{
   id: "Qm9vay0x",
   name: "Jim Halpert"
}

I'm following this example: https://github.com/zeit/next.js/tree/master/examples/with-react-relay-network-modern, with one change in _app.js: I'm getting the queryID in the render function by Component.query().default.params.name instead of Component.query().params.name. Here are my dependencies:

"react-relay": "^5.0.0",
"react-relay-network-modern": "^4.0.4",
"react-relay-network-modern-ssr": "^1.2.4",

My theory is that when rendered on the server side, createFragmentContainer tries to render the component with the exact props it was passed with that contains all the relay metadata, instead of the data that the fragment defines.

juhaelee avatar Aug 14 '19 05:08 juhaelee

+1 @HsuTing this also means that refetchContainer and paginationContainer don't work when following your next.js example.

I'm just trying to render a simple blog page:

import React from "react"
import Relay, { graphql } from "react-relay"

const fragment = graphql`
    fragment blog_pages on Query
        @argumentDefinitions(
            count: { type: "Int", defaultValue: 10 }
            cursor: { type: "String" }
        ) {
        blogPages(first: $count, after: $cursor)
            @connection(key: "blog_blogPages") {
            edges {
                node {
                    title
                }
            }
            pageInfo {
                hasNextPage
                endCursor
            }
        }
    }
`

const query = graphql`
    query blog_BlogIndexPageQuery($count: Int!, $cursor: String) {
        ...blog_pages @arguments(count: $count, cursor: $cursor)
    }
`

class TestDiv extends React.Component {
    render() {
        console.log(this.props)
        return <div />
    }
}

const FragmentContainer = Relay.createFragmentContainer(TestDiv, {
    pages: fragment
})

FragmentContainer.query = query
FragmentContainer.getInitialProps = async ctx => {
    return {
        variables: {
            count: 10,
            cursor: null
        }
    }
}

export default FragmentContainer

but on both the server and client i'm getting the the warning:

Warning: createFragmentSpecResolver: Expected prop `pages` to be supplied to `Relay(TestDiv)`, but got `undefined`. Pass an explicit `null` if this is intentional.

and the props:

{
  __fragments: { blog_pages: { count: 10, cursor: null } },
  __id: 'client:root',
  __fragmentOwner: {
    identifier: 'query blog_BlogIndexPageQuery(\n' +
      '  $count: Int!\n' +
      '  $cursor: String\n' +
      ') {\n' +
      '  ...blog_pages_1G22uz\n' +
      '}\n' +
      '\n' +
      'fragment blog_pages_1G22uz on Query {\n' +
      '  blogPages(first: $count, after: $cursor) {\n' +
      '    edges {\n' +
      '      node {\n' +
      '        title\n' +
      '        id\n' +
      '        __typename\n' +
      '      }\n' +
      '      cursor\n' +
      '    }\n' +
      '    pageInfo {\n' +
      '      hasNextPage\n' +
      '      endCursor\n' +
      '    }\n' +
      '  }\n' +
      '}\n' +
      '{"count":10,"cursor":null}',
    node: {
      kind: 'Request',
      fragment: [Object],
      operation: [Object],
      params: [Object],
      hash: '97997107df67905d085d233d494694d7'
    },
    variables: { count: 10, cursor: null }
  },
  pages: null,
  relay: {
    environment: RelayModernEnvironment {
      configName: undefined,
      __log: [Function: emptyFunction],
      _defaultRenderPolicy: 'full',
      _operationLoader: undefined,
      _operationExecutions: Map(0) {},
      _network: [Object],
      _getDataID: [Function: defaultGetDataID],
      _publishQueue: [RelayPublishQueue],
      _scheduler: null,
      _store: [RelayModernStore],
      options: undefined,
      __setNet: [Function (anonymous)],
      DEBUG_inspect: [Function (anonymous)],
      _missingFieldHandlers: undefined,
      _operationTracker: [RelayOperationTracker]
    }
  }
}

Any ideas on how to deal with this?

stan-sack avatar Feb 27 '20 11:02 stan-sack

To follow on from that, if I just do the query without the fragment it works fine:

import React from "react"
import BlogIndexPageQuery from "shared-js/queries/BlogIndexPageQuery"

class TestDiv extends React.Component {
    render() {
        console.log(this.props)
        return <div />
    }
}

TestDiv.query = graphql`
    query BlogIndexPageQuery {
        blogPages {
            edges {
                node {
                    title
                    body
                }
            }
        }
    }
`

export default TestDiv

stan-sack avatar Feb 27 '20 12:02 stan-sack

@juhaelee i just worked out the issue. You need to wrap your fragmentContainer in a higher order component which passes in the props as the expected fragment. its not an issue with the library. Please see how I achieved this below:

import React from "react"
import BlogIndexPageQuery from "shared-js/queries/BlogIndexPageQuery"
import { createPaginationContainer, graphql } from "react-relay"

class TestDiv extends React.Component {
    render() {
        console.log(this.props)
        return <div />
    }
}

const query = graphql`
    # Pagination query to be fetched upon calling 'loadMore'.
    # Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
    query blogPageIndexQuery($count: Int!, $cursor: String) {
        ...blog_pages @arguments(count: $count, cursor: $cursor)
    }
`

const TestPagContainer = createPaginationContainer(
    TestDiv,
    {
        pages: graphql`
            fragment blog_pages on Query
                @argumentDefinitions(
                    count: { type: "Int", defaultValue: 10 }
                    cursor: { type: "String" }
                ) {
                blogPages(first: $count, after: $cursor)
                    @connection(key: "blog_blogPages") {
                    edges {
                        node {
                            title
                        }
                    }
                }
            }
        `
    },
    {
        direction: "forward",
        getConnectionFromProps(props) {
            return props.pages && props.pages.blogPages
        },
        // This is also the default implementation of `getFragmentVariables` if it isn't provided.
        getFragmentVariables(prevVars, totalCount) {
            return {
                ...prevVars,
                count: totalCount
            }
        },
        getVariables(props, { count, cursor }, fragmentVariables) {
            return {
                count,
                cursor
            }
        },
        query: query
    }
)

const PagWrapper = props => (
    <TestPagContainer
        pages={props}
        // user={this.props.user}
        // // clientPaymentToken={this.props.clientPaymentToken}
    />
)

PagWrapper.query = query
PagWrapper.getInitialProps = async () => {
    return {
        variables: {
            count: 10,
            cursor: null
        }
    }
}
export default PagWrapper

stan-sack avatar Feb 29 '20 05:02 stan-sack

FWIW this may only be required when following https://github.com/zeit/next.js/tree/master/examples/with-react-relay-network-modern

stan-sack avatar Feb 29 '20 05:02 stan-sack

@stan-sack You should modify the _app.js, depending on your actual case. _app.js will give the all data to the props. In your case, a prop named pages is required in createFragmentContainer, but the props will look like { blogPages: { ... } } in blog.js.

Maybe you can try this:

// _app.js

...
  return (
    <QueryRenderer
        environment={environment}
        query={Component.query}
        variables={variables}
        render={({ error, props }) => {
          if (error) return <div>{error.message}</div>
          else if (props) return <Component pages={props} />
          return <div>Loading</div>
        }}
      />
  );
...

HsuTing avatar Feb 29 '20 07:02 HsuTing

@juhaelee Is the problem resolved? Maybe you can give the reproduce repo?

HsuTing avatar Feb 29 '20 07:02 HsuTing

Was actually stuck on this for quite some time, and the above explanation didn't really help.

Found this StackOverflow article which describes the same problem and reasoning why it returns a weird payload instead of the data. https://stackoverflow.com/questions/52145389/relay-queryrenderer-does-not-return-expected-props-from-query

Relay encapsulates data by fragments. You can see data only if they are included in current component fragment (see https://facebook.github.io/relay/docs/en/thinking-in-relay#data-masking)

Basically, if you pass that reference to a child which has a createFragmentContainer, the data will be loaded and passed through.

Sicria avatar Mar 20 '20 08:03 Sicria