relay-tools / react-relay-network-modern-ssr

SSR middleware for react-relay-network-modern
https://github.com/relay-tools/react-relay-network-modern-ssr
MIT License
67 stars 10 forks source link

Components with createFragmentContainer not resolving fields #16

Open juhaelee opened 5 years ago

juhaelee commented 5 years ago

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.

stan-sack commented 4 years ago

+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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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

HsuTing commented 4 years ago

@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 commented 4 years ago

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

Sicria commented 4 years ago

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.