Search code examples
react-relay

React Relay createFragmentContainer and QueryRenderer data flow


I've been working through the examples in relay's document for QueryRenderer https://relay.dev/docs/en/query-renderer and FragmentContainer https://relay.dev/docs/en/fragment-container

I'm confused as to how the data is meant to be accessed by the Components wrapped in the HOC createFragmentContainer.

I have a top level QueryRenderer:

function renderFunction({error, props}) {
...
  if (props) {
    // Added todoList as a prop with a distinct name from tdlist which
    // is meant to be passed in by createFragmentContainer
    return <TodoList todoList = {props.todoList} />
  }
...
}
export default function ContainerWithQueryRenderer() {
  return (
    <QueryRenderer
      environment={environment}
      query={graphql`
        query ContainerWithQueryRenderer_Query {
          todoList {
            title
            todoListItems { text isComplete}
          }
        }`}
      render = { renderFunction }
    />
  );
}

and a TodoList component that defines what data is needed in a graphql fragment:

import React from 'react';
import TodoItem from './TodoItem.js';
import { createFragmentContainer } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
function TodoList(props) {
  console.log('TodoList props:',props);
  if (props && props.todoList && props.todoList.todoListItems && props.todoList.title) {
    return (
      <div className='list-container'>
        <div className='list-header'>
          <div className='list-header-label'>{props.todoList.title}</div>
        </div>
        <div className='list'>{props.todoList.todoListItems.map( (item, key) => <TodoItem key = {key} item={item} />)}</div>
      </div>
    );
  } else {
    return null;
  }
}
export default createFragmentContainer(TodoList,{
  tdlist: graphql`
    fragment TodoList_tdlist on TodoList {
      title
      todoListItems {
        ...TodoItem_item
      }
    }
  `,
})

and a child TodoListItem

import React from 'react';
import { createFragmentContainer } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
function TodoItem(props) {

  return <div className='list-item'>
      <div className='list-item-label'>{props.item.text}</div>
      {props.item.isComplete ?<div className='list-item-buttons'>Done</div>:null}
    </div>
}

export default createFragmentContainer( TodoItem,{
  item: graphql`
    fragment TodoItem_item on Todo {
      text isComplete
    }
  `
});

My understanding is that the createFragmentContainer for the TodoList will inject the data from the TodoList_tdlist fragemnt as the TodoList props.tdlist), shaped as per the shape of the graphql query.

However, this appears to not be happening. I get a warning in the console:

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

What is the job of the createFragmentContainer if it is not to pass in the tdlist?

I tried to pass the todoList data explicitly in by changing return to return (the same prop name passed in by createFragmentContainer, I (understandably) get a different error:

Warning: RelayModernSelector: Expected object to contain data for fragment `TodoList_tdlist`, got `{"title":"Your to-do list","todoListItems":[{"text":"Brush teeth","isComplete":false},....

I think the root of my confusion is not understanding how the fragments that define the data dependencies interact with the QueryRenderer. Do I need to define the query to pull in every possible piece of data that could ever be needed, and the point is that relay will only query what is needed by looking at the graphql fragments of the components that are being rendered now, and will re-query if that changes, updating props as it gets new data?

Do I need to pass props down to fragment containers as props explicitly, or if they are a descendant of a QueryRenderer that requests their data, will the createFragmentContainer be able to access it via the relay environment?

Here is my graphql.schema to assist:

type Query {
  todoList: TodoList!
}
type Todo {
  text: String!
  isComplete: Boolean!
}
type TodoList {
  title: String!
  todoListItems: [Todo]!
}

Solution

  • You must name the descendant fragment container's GraphQL fragment in your QueryRenderer. Without identifying the fragment, react-relay has relay no link between the QueryRenderer and the descendant FragmentContainer components. In your question, the query prop passed to QueryRenderer is

    query ContainerWithQueryRenderer_Query { todoList { title todoListItems { text isComplete }}}

    instead of

    query ContainerWithQueryRenderer_Query { TodoList_tdlist }

    Also, because the QueryRenderer works hand in hand with the Fragment Containers, it is simpler to iterate the todoListItems in the Query Renderer, and use a fragment for the TodoItem_item. Hence below I have merged the above <ContainerWithQueryRenderer /> and <TodoList />

    This approach works:

    TodoListWithQueryRenderer.js:

    function renderFunction({error, props}) {
    ...
      if (props) {
        return (
            <div>
              <div>
                <div>{props.todoList.title}</div>
              </div>
              <div>{props.todoList.todoListItems.map( (item, key) => <TodoItem key = {key} item={item} /> )}</div>
            </div>
          );
      }
    ...
    }
    export default function TodoListWithQueryRenderer() {
      return (
        <QueryRenderer
          environment={environment}
          query={graphql`
            query TodoListWithQueryRenderer_Query { todoList {todoListItems {...TodoItem_item } title} }
          `}
          render = { renderFunction }
        />
      );
    }
    

    with only one descendant component required as the above merges the ContainterWithQueryRenderer with TodoList components.

    TodoItem.js:

    import React from 'react';
    import { createFragmentContainer } from 'react-relay';
    import graphql from 'babel-plugin-relay/macro';
    function TodoItem(props) {
    
      return <div>
          <div>{props.item.text}</div>
          {props.item.isComplete ? <div>Done</div> : null}
        </div>
    }
    
    export default createFragmentContainer( TodoItem,{
      item: graphql`
        fragment TodoItem_item on Todo {
          text isComplete
        }
      `
    });