Relay Modern RefetchContainer props aren't passed to component
Asked Answered
U

2

13

I'm having some issues setting up a refetchContainer in Relay Modern. A parent component is the QueryRenderer, which runs an initial query, populating the child component's props appropriately (a-prop-riately? eh? eh?!) . The refetchContainer specifies all our variables, and on an input field's onChange event, re-runs a query with the new variables. This all works perfectly, except that the child's props are never updated with the new data received. I can drill down the Relay store and see that the query was indeed received with the appropriate data. Been bangin' my head against this for a while and I would appreciate some help. Probably something simple I'm missing. And Lord knows Relay Modern documentation is sparse.

I've poked around and can't find an appropriate solution. This guy seems to be having a similar issue: relay refetch doesn't show the result

The parent component with QueryRenderer:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { graphql, QueryRenderer } from 'react-relay';
import Search from './Search';

const propTypes = {
  auth: PropTypes.object.isRequired,
};

class SearchContainer extends Component {
  render() {
    return (
      <QueryRenderer
        query={graphql`
          query SearchContainerQuery($search: String!){
            users: searchUsers(search:$search, first:10){
              ...Search_users
            }
          }`}
        variables={{ search: 'someDefaultSearch' }}
        environment={this.props.auth.environment}
        render={({ error, props }) => {
          if (error) {
            console.log(error);
          }
          if (props) {
            return <Search users={props.users} />;
          }
          return <div>No Dice</div>;
        }}
      />
    );
  }
}

SearchContainer.propTypes = propTypes;

export default connect(state => ({ auth: state.auth }))(SearchContainer);

The child component with createRefetchContainer:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { createRefetchContainer, graphql } from 'react-relay';

const propTypes = {
  relay: PropTypes.object.isRequired,
  users: PropTypes.object,
};

const defaultProps = {
  users: {},
};

class Search extends Component {
  render() {
    return (
      <div>
        <input
          type="text"
          onChange={(e) => {
            e.preventDefault();
            this.props.relay.refetch({
              search: e.target.value,
            });
          }}
        />
        <ul>
          {this.props.users.nodes.map(user =>
            <li key={user.id}>{user.username}</li>,
          )}
        </ul>
      </div>
    );
  }
}

Search.propTypes = propTypes;
Search.defaultProps = defaultProps;

export default createRefetchContainer(
  Search,
  {
    users: graphql.experimental`
      fragment Search_users on SearchUsersConnection
      @argumentDefinitions(
        search: {type: "String", defaultValue: ""}
      ) {
        nodes {
            id
            displayName
            username
          }
      }
    `,
  },
  graphql.experimental`
    query SearchRefetchQuery($search: String!) {
      users: searchUsers(search:$search, first:10){
        ...Search_users @arguments(search: $search)
      }
    }
  `,
);

GraphQL looks like this:

# A connection to a list of `User` values.
type SearchUsersConnection {
  # Information to aid in pagination.
  pageInfo: PageInfo!

  # The count of *all* `User` you could get from the connection.
  totalCount: Int

  # A list of edges which contains the `User` and cursor to aid in pagination.
  edges: [SearchUsersEdge]

  # A list of `User` objects.
  nodes: [User]
}

Network calls are made appropriately, and data is returned as expected. NetworkCalls

It seems the @arguments directive can be left out of the refetch query here:

query SearchRefetchQuery($search: String!) {
      users: searchUsers(search:$search, first:10){
          ...Search_users @arguments(search: $search)
      }
}

(removing it seems to have no effect)

I've tried adding the @arguments directive to the parent component's fragment as per the recommendation here: Pass variables to fragment container in relay modern, to no effect.

Uncinate answered 21/7, 2017 at 19:48 Comment(7)
sorry not much help.. fwiw I have dropped refetchContainers completely for simply passing new props to the QueryRenderer which gets the job done. example: github.com/NCI-GDC/portal-ui/blob/next/src/packages/@ncigdc/…Palmitin
@Palmitin Yep, that seems to work just fine. It just bothers me that the refetchContainer, specifically designed for this, is so tricky to set up!Uncinate
I'm having the exact same issue and I am TOO banging my head on the wall :(Ermina
I have an almost identical query working fine and can't find any apparent errors in your code. Did you try upgrading to a more recent version? ... or just maybe, try unwrapping it from redux just to see if it works on its own...Gristede
Same problem and I have tried all sorts of variations. @Gristede I'm using most recent version and have tried that.Incarcerate
Oh I think I got through a similar problem too. If I remember correctly I believe the "id" Relay is using to identify the component is including the variables for the initial query so try unwrapping your container fragment in the QueryRenderer by moving the searchUsers(..) to the fragment and then in the query just ...Search_user. You'll have to define Search_user as a fragment of your root, i.e.: in my case it'd something like fragment Search_user on RootQueryType. My last suggestion is turning your debugger on and see whats going on when you get the data.Gristede
@Gristede Wow, moving the query to the fragment worked. This has to be a bug, if not then the example posted on facebook examples should not work. Post an answer and i'll credit you the bounty. Thanks so much. Here's the issue I raised: github.com/facebook/relay/issues/2244Incarcerate
R
6

As mentioned in other answers, this is actually expected behavior, and I'll try to expand on the answer a little bit.

When refetch is called and the refetchQuery is executed, Relay doesn't actually use the result of the query to re-render the component. All it does is normalize the payload into the store and fire any relevant subscriptions. This means that if the fetched data is unrelated to the data that the mounted container is subscribed to (e.g. using a totally different node id that doesn't have any data overlaps), then the component won't re-render.

In this specific scenario, the reason why the container doesn't re-render, i.e. why it isn't subscribed to the changes fired by the refetchQuery, is due to how subscriptions are set up. A subscription is basically a subscription to a snapshot, which basically represents a concrete fragment and the data and records associated with it at a given point in time -- when the records associated with a snapshot change, the subscription for that snapshot will be notified of changes.

In this case, given that the fragment for the container has no variables, the snapshot that it will subscribe to will only be associated with the initial node; after refetch happens and the the store is updated, the records for the new node will have been updated, but the record associated with that original snapshot hasn't changed, so the container subscribed to it won't be notified.

This means that Refetch containers are only really meant to be used when you are changing variables in the component fragment. The solutions provided here are valid, i.e. including variables in the fragment for the refetch container or setting new variables one level up in the QueryRenderer.

This could definitely be made more clear in the docs, so I'll update them to reflect this. I hope this helps!

Rhodes answered 3/1, 2018 at 16:25 Comment(0)
G
5

Oh I think I got through a similar problem. If I remember correctly I believe the "id" Relay is using to identify the component to pass the props to, is including the variables for the initial query so try unwrapping your container fragment from the QueryRenderer's query.

The setup that worked for me is something like the following:

<QueryRenderer
  variables={{search: 'someDefaultSearch', count: 10}}
  query={graphql`
    query SearchContainerQuery($search: String!, $count: Int!){
      # if we want defaults we need to "prepare" them from here
      ...Search_users @arguments(search: $search, count: $count)
    }
  `}
  environment={this.props.auth.environment}
  render={({ error, props: relayProps }) => {
    // I use relayProps to avoid possible naming conflicts
    if (error) {
      console.log(error);
    }
    if (relayProps) {
      // the users props is "masked" from the root that's why we pass it as is
      return <Search users={relayProps} />;
    }
    return <div>No Dice</div>;
  }}
/>

And the refetchContainer would look something like:

export default createRefetchContainer(
  Search,
  {
    users: graphql`
      # In my schema the root is called RootQueryType 
      fragment Search_users on RootQueryType
      @argumentDefinitions(
        search: {type: "String"}
        count: {type: "Int!"}
      ) {
        searchUsers(search: $search, first: $count) {
          nodes {
            id
            displayName
            username
          }
        }
      }
    `
  },
  graphql`
    query SearchRefetchQuery($search: String!, $count: Int!) {
      ...Search_users @arguments(search: $search, count: $count)
    }
  `
);

Notice that the above example assumes Relay v1.3 where graphql.experimental is deprecated. Also, I don't remember if with the @arguments trick it's possible to make your approach of aliasing the searchUsers work.

My last suggestion is turning your debugger on and see whats going on when you get the data.

Finally, as per your comments, I agree this might be a BUG. Let's see how things evolve in the issue you reported.

Gristede answered 22/12, 2017 at 20:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.