facebook / relay

Relay is a JavaScript framework for building data-driven React applications.
https://relay.dev
MIT License
18.4k stars 1.82k forks source link

Modern: Recursion in static query fragments #1998

Closed staylor closed 4 years ago

staylor commented 7 years ago

I would like to spread a fragment within a fragment, but this is disallowed by the compiler:

  fragment ContentNode_content on ContentNode @relay(plural: true) {
    __typename
    ... on Embed {
      ...Embed_node
    }
    ... on Text {
      text
    }
    ... on Element {
      ...Element_node
      children {
        __typename
        ...ContentNode_content
      }
    }
  }

See full code here: https://github.com/staylor/wp-relay-app/blob/master/src/components/ContentNode/index.js

In lieu of spreading the fragment, I have recursively included nodes at a limited depth. How might this be accomplished with static queries?

The SDL is:

# The content for the object.
type Content {
  # HTML for the object, transformed for display.
  rendered: String

  # Content for the object, as it exists in the database.
  raw: String

  # Whether the field is protected with a password.
  protected: Boolean

  # Content HTML as structured data
  data: [ContentNode]
}

union ContentNode = Element | Text | Embed

# An element node.
type Element {
  tagName: String
  attributes: [Meta]
  children: [ContentNode]
}

# A text node.
type Text {
  text: String
}

# An embed node.
type Embed {
  version: String
  title: String
  html: String
  providerUrl: String
  providerName: String
  authorName: String
  authorUrl: String
  thumbnailUrl: String
  thumbnailWidth: Int
  thumbnailHeight: Int
  width: Int
  height: Int
}
sibelius commented 7 years ago

what is the error?

staylor commented 7 years ago
Error: You supplied a GraphQL document with validation errors:
$ROOT/src/components/ContentNode/index.js:
Cannot spread fragment "ContentNode_content" within itself.
sibelius commented 7 years ago

you could try to use @inline experimental directive to achieve this

robrichard commented 7 years ago

The error is because the ContentNode_content fragment is spread inside of itself. This is not allowed by the GraphQL spec: https://facebook.github.io/graphql/#sec-Fragment-spreads-must-not-form-cycles

If there was a cyclical relationship between ContentNode objects you would be requesting an infinite amount of data

staylor commented 7 years ago

@sibelius getting Invariant Violation: RelayParser: Unknown directive @inline

sibelius commented 7 years ago

https://github.com/facebook/relay/blob/master/docs/modern/Directives.md#inline

staylor commented 7 years ago

I am using Relay in modern mode, I see that directive in the docs, it also doesn't work if I try graphql.experimental

robrichard commented 7 years ago

You need to install v1.2.0-rc.1 for @inline to work. But I still don't think it will solve your issue as it's not allowed by the GraphQL spec because you are requesting potentially an infinite amount of data from your GraphQL server.

sibelius commented 7 years ago

@femesq is doing something like this

staylor commented 7 years ago

the spreading is nice, was able to add it: https://github.com/staylor/wp-relay-app/blob/2d9d0f752e9764cbfdee9dc38f7216507c5aecb9/src/components/ContentNode/index.js

staylor commented 7 years ago

Another thing to note: this is reallly an opaque structure (HTML parsed into JSON) - there is never a time when I only want some of it.

femesq commented 7 years ago

For my use-case, I've opted to "fetch" the first 4 levels of my tree, and their children are 'fetched' on demand using PaginationContainers... This can lead to many query cascading, depending on the user's tree-structure, but this approach solves the most of user's arrangement fine...

sibelius commented 7 years ago

you can solve this using a QueryRenderer

tim-field commented 7 years ago

QueryRenderer example, remove the toggle control and it'll fire off as many queries as required to load the whole tree, which is probably a bad idea.

import React, {Component} from 'react'
import Immutable from 'immutable'
import { createFragmentContainer, graphql, QueryRenderer } from 'react-relay'

const CategoryTreeQuery = graphql`
  query CategoryTreeQuery(
    $parent: Int!
  ) {
    query {
     ...CategoryTree_query
    }
  }
`;

class CategoryTree extends Component {

  state = {
    expanded: Immutable.Set()
  }

  toggle = (e, id) => {
    e.preventDefault();
    const has = this.state.expanded.includes(id)
    const expanded = has ? this.state.expanded.delete(id) : this.state.expanded.add(id)
    this.setState({expanded})
  }

  render() {
    const { query: { categories }, relay } = this.props
    const { expanded } = this.state
    return categories.edges ? (
      <ul>
        {categories.edges.map(({ node: category }) => (
          <li key={category.id}>
            <a onClick={(e) => this.toggle(e, category.id)}>{category.name}</a>
            {expanded.includes(category.id) &&
              <QueryRenderer
                environment={relay.environment}
                query={CategoryTreeQuery}
                variables={{
                  parent: category.term_id
                }}
                render={({ error, props }) => {
                  if (error) {
                    return <div>{error.message}</div>
                  } else if (props) {
                    return <CategoryTreeRelay {...props} />
                  }
                  return <div>Loading</div>
                }}
              />
            }
          </li>
        )
        )}
      </ul>
    ) : null
  }
}

const CategoryTreeRelay = createFragmentContainer(
CategoryTree,
graphql`
  fragment CategoryTree_query on Query {
    categories: terms (
      first: 1000
      parent: $parent
    ) {
      edges {
        node {
          id
          term_id
          name
        }
      }
    }
  }
`
)

export default CategoryTreeRelay
staylor commented 7 years ago

My example is about representing HTML as a tree structure that can have deep nesting. It is just a field, not a paginated result set.

<section>
  <header>
     <h1>
       <strong>Warning:</strong> This can get deeply nested. <a href="">Even in <span>Here,
 <em>riiiight</em>.</span>.</a>
    </h1>
  </header>
  <p>More Text</p>
</section>

There is no way I only want some levels of this data, I always need all of it. In the GQL query, assuming all nodes have a type, I need to do this to reach em:

node {
  tagName // section
  children {
    tagName // header
    children {
      tagName // h1
      children {
        tagName // a
        children {
          tagName // span
          children {
            tagName // em
            children {
              text
              ... this could go on for many more levels in a table or something 
            } 
          }
        }
      }
    }
  }
}

It would nice if recursion could be introduced somewhere in here - the problem lies with children being a list of the same type as the item including it

sibelius commented 5 years ago

@staylor try to follow @tim-field answer https://github.com/facebook/relay/issues/1998#issuecomment-330367298

that idea is to only call another query renderer if needed

the other way to resolve this, is to change the resolvers structure

josephsavona commented 4 years ago

GraphQL doesn't support recursive fragments by design, as this could allow unbounded data-fetching. Relay goes a bit further and offers support for recursion, but it still has to be terminated - you can use @argumentDefinitions to define a boolean value that is used to conditionally include the same fragment, passing @arguments to change the condition. But the recursion still has to terminate statically - e.g. you can have a fixed number of levels of recursion.

I'm going to close as this is an intentional restriction that we don't plan on changing.