Relay: recursive data

Created on 6 Sep 2015  ·  14Comments  ·  Source: facebook/relay

First off all, love idea and the great talks about it on ReactEurope. But I still get stuck quite a lot (which I should expect from a technical preview). Here is my current problem:

My data is modeled like this:

type Babushka {
  id: !String,
  name: !String,
  babushkas: [Babushka]
}

Now I want to display a Babushka list for which I have a List and a ListItem Component:

list.js

class BabushkaList extends Component {

  render() {
    return <ul>{this.props.babushka.babushkas.map(b => <BabushkaListItem babushka={b}/>)}</ul>;
  }

}

export default Relay.createContainer(BabushkaList, {

  fragments: {
    babushka: () =>  Relay.QL`
      fragment on Babushka {
        babushka {
          ${BabushkaListItem.getFragment('babushka')}
        }
      }
    `
  }

});

list-item.js

class BabushkaListItem extends Component {


  render() {
    return (
      <li onClick={this.onClick.bind(this)}>
        {this.props.babushka.name}
        <ul><BabushkatList babushka={this.props.babushka}</ul>
      </li>
    );
  }

}

export default Relay.createContainer(BabushkaListItem, {

  fragments: {
    babushka: () =>  Relay.QL`
      fragment on Babushka{
        name,
        ${BabushkaList.getFragment('babushka')}
      }
    `
  }

});

Naturally this leads to the stack size being exceeded. Am I doing something wrong fragment wise?

Most helpful comment

@josephsavona Hey, I tried converting your example for specifying 1 level deep for relay-modern but I still get the error:

comments/commentContainer.js: Cannot spread fragment "commentContainer_comment" within itself via repliesContainer.

CommentContainer.js:

const fragments = graphql`
  fragment commentContainer_comment on Comment {
    commentId
    body
    dateAdded
    likes
    dislikes
    originalComment {
      commentId
    }
    user {
      userName
    }
    ...repliesContainer
  }
`;

RepliesContainer.js:

const fragments = graphql`
  fragment repliesContainer on Comment
  @argumentDefinitions(
    count: { type: "Int", defaultValue: 3 }
    expandReplies: { type: "Boolean"}
  ) {
    id
    replies(
      first: $count
      after: $cursor
    ) {
      totalCount
      items {
        commentId
        ...commentContainer_comment @include (if: $expandReplies)
      }
    }
  }
`;

And then in my root query I pass down expandReplies: true.

Do you know what I am doing wrong? I only want to include commentContainer_comment once and not infinitely.

All 14 comments

Recursive fragments are supported so long as you explicitly specify a stopping point. The pattern for this is to create a variable that determines if the nested item should be expanded or not - default it to false, and then only "expand" the items one level deep to begin with:

export default Relay.createContainer(BabushkaListItem, {
  initialVariables: {
    expand: false, // use `this.props.relay.setVariables({expand: true})` to expand an additional level
  },
  fragments: {
    babushka: (variables) =>  Relay.QL`  # note the `variables` argument here
      fragment on Babushka{
        name,
        ${BabushkaList.getFragment('babushka').if(variables.expand)} # getFragment().if(variables.foo)
      }
    `
  }
});

The parent component of this list can then specify that the first level of items should be expanded initially:

Relay.createContainer(Foo, {
  fragments: {
    foo: () => Relay.QL`
      fragment on Foo {
        ${BabushkaList.getFragment('babushka', {expand: true})} #override the default value to expand a single level of the list
      ...

Just what I was looking for! Thanks a lot Joesph.

Now I'm getting the following error:

Warning: RelayContainer: Expected prop `babushka` supplied to `BabushkaList` to be data fetched
by Relay. This is likely an error unless you are purposely passing in mock data that conforms to
the shape of this component's fragment.

Which doesn't make a whole lot of sense to me, because the babushka being passed to the List component is obviously fetched by Relay and not mocked by me. Am I missing something?

@Gregoor is it possible to link to a gist of the code for the parent and child component?

Does this suffice?

I can't really make sense of the error in that context. :( Thanks for getting back to me so quickly

Commented on the gist, but since SpeciesList expects the species prop to be an array of species, it should mark the fragment as plural: fragment on Species @relay(plural:true) {...}.

Out of all the words in the english language, I pick one where the singular and plural is isomorphic. Oh brain, why?

Anyway thanks, didn't know about the annotations(?). Are they already documented somewhere?
But I do think that species is singular in both cases, since the list component fetches the subspecies itself (which should be available).

I changed the naming once again to something more familiar and heteromorphic.

Hi @Gregoor @josephsavona ...,
I'm a little bit confused. React, Relay and GraphQL are quite new for me.
Is it possible for example to render recursively a directory structure with n descendants in a single http request?
If not, what's the source of the "problem"?

  • GraphQL doesn't provide a syntax to query recursively data
  • or Relay doesn't know how to use this GraphQL syntax.

Relay batch the queries so in the worst case scenario, if I understand, I'll have a single GraphQL Query per level and by using a condition like ${BabushkaList.getFragment('babushka') we can tweak the thing to get n levels at the same time.

Thanks for clarifications.

@grifx: Instead of a boolean you can use a number that you decrement every level of recursion. If it's <= 0 you don't expand.

What is the deal with #if() in BabushkaList.getFragment('babushka').if(variables.expand)? It looks like RelayContainer.getFragment() returns a RelayQueryFragment which doesn't seem to have an #if() on its prototype.

* Edit *

Posted too soon: it's a RelayFragmentReference. Will continue investigating but any guidance regarding where I might read (non-code) about this would be great.

@dminkovsky RelayFragmentReference and the if/unless functions on it aren't documented. The plan is to replace these with standard GraphQL @include/@skip directives so that you could write something like:

fragment on Foo {
  ${Child.getFragment('foo')} @include(if: $var) @relay(variables: {foo: $bar})
}

@joesavona I was wondering just that—the relationship between if/unless and
directives. Thanks for clarifying!

понедельник, 4 января 2016 г. пользователь Joseph Savona написал:

@dminkovsky https://github.com/dminkovsky RelayFragmentReference and
the if/unless functions on it aren't documented. The plan is to replace
these with standard GraphQL @include/@skip directives.


Reply to this email directly or view it on GitHub
https://github.com/facebook/relay/issues/246#issuecomment-168863730.

@josephsavona Hey, I tried converting your example for specifying 1 level deep for relay-modern but I still get the error:

comments/commentContainer.js: Cannot spread fragment "commentContainer_comment" within itself via repliesContainer.

CommentContainer.js:

const fragments = graphql`
  fragment commentContainer_comment on Comment {
    commentId
    body
    dateAdded
    likes
    dislikes
    originalComment {
      commentId
    }
    user {
      userName
    }
    ...repliesContainer
  }
`;

RepliesContainer.js:

const fragments = graphql`
  fragment repliesContainer on Comment
  @argumentDefinitions(
    count: { type: "Int", defaultValue: 3 }
    expandReplies: { type: "Boolean"}
  ) {
    id
    replies(
      first: $count
      after: $cursor
    ) {
      totalCount
      items {
        commentId
        ...commentContainer_comment @include (if: $expandReplies)
      }
    }
  }
`;

And then in my root query I pass down expandReplies: true.

Do you know what I am doing wrong? I only want to include commentContainer_comment once and not infinitely.

Was this page helpful?
0 / 5 - 0 ratings