facebook / relay

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

Can a fragment container be defined to render a list of elements, rather than a single element? #3183

Closed Hubro closed 4 years ago

Hubro commented 4 years ago

I have defined a fragment container that should render a list of elements:

export const FormInstanceListContainer: FunctionComponent<Props> = props => {
  const { data, ...rest } = props
  const { formInstances } = data

  return (
    <FormInstanceList
      formInstances={formInstances}
      {...rest} // Forward all the other props
    />
  )
}

export default createFragmentContainer(FormInstanceListContainer, {
  data: graphql`
    fragment FormInstanceListContainer_data on RootQuery
      @argumentDefinitions(status: { type: "FormStatus" }) {
      formInstances(status: $status) {
        id
        form {
          id
          name
        }
        status
        createdAt
        submittedAt
      }
    }
  `,
})

This follows the "composing fragment containers" example from your docs: https://relay.dev/docs/en/quick-start-guide#composing-fragments

However, the docs don't mention how you would render more than one of these at the same depth. I obviously can't do this:

query pages_dashboard_Query {
  ...FormInstanceListContainer_data @arguments(status: DRAFT)
  ...FormInstanceListContainer_data @arguments(status: SUBMITTED)
}

There also doesn't appear to be a way to use aliases to separate the queries when using fragment containers (https://github.com/facebook/relay/issues/1978).

This means that I have to move the queries out of the fragment container, so now I have:

export default createFragmentContainer(FormInstanceListContainer, {
  formInstance: graphql`
    fragment FormInstanceListContainer_formInstance on FormInstance
      id
      form {
        id
        name
      }
      status
      createdAt
      submittedAt
    }
  `,
})

And my main query is now:

query pages_dashboard_Query {
  draftFormInstances: formInstances(status: DRAFT) {
    ...FormInstanceListContainer_formInstance
  }
  submittedFormInstances: formInstances(status: SUBMITTED) {
    ...FormInstanceListContainer_formInstance
  }
}

The issue is that now my FormInstanceListContainer is only requesting a single FormInstance, not a list of them. If I try to pass it the entire list, Relay crashes with an error about invalid input (truncated):

RelayModernSelector: Expected value for fragment FormInstanceListContainer_formInstances to be an object, got `[{"__fragments":{"FormInstanceListContainer_formInstances":{}}," ...

Is there any way to instruct Relay that FormInstanceListContainer should render a list of FormInstance, not just one?

Am I completely lost here? Should I be taking a completely different approach that I'm not seeing?

Hubro commented 4 years ago

I have been completely stuck on this for hours, so I've worked around this the only way I see how - I have refactored my fragment container by extracting the list item out into a separate component. The list item is now the fragment container, not the list itself. The consequence of this is that the list itself can no longer access any of the queried data, since it's not a query fragment... So all the logic it used to do has to be moved up into the parent component, which in this case is my index page:

/*
 * Dashboard page
 */

import { Typography, Container } from "@material-ui/core"
import { NextPage } from "next"
import { graphql } from "react-relay"
import NextRouter from "next/router"

import DefaultLayout from "../components/layout/DefaultLayout"
import FormInstanceList from "~/components/forms/FormInstanceList"
import FormInstanceListItemContainer from "~/components/forms/FormInstanceListItemContainer"
import withData from "~/withData"

import {
  pages_dashboard_Query,
  pages_dashboard_QueryResponse as QueryResponse,
} from "./__generated__/pages_dashboard_Query.graphql"

const IndexPage: NextPage<QueryResponse> = props => {
  const handleOpenClick = (
    formInstance: typeof props.draftFormInstances[0]
  ) => {
    NextRouter.push(`/form-instance/${formInstance.id}`)
  }

  const handleEditClick = (
    formInstance: typeof props.draftFormInstances[0]
  ) => {
    NextRouter.push(`/form-instance/${formInstance.id}/edit`)
  }

  const { draftFormInstances, submittedFormInstances } = props

  return (
    <DefaultLayout>
      <Container maxWidth="md">
        <Typography variant="h5" style={{ marginBottom: 25 }}>
          My drafts
        </Typography>

        <FormInstanceList>
          {submittedFormInstances.length == 0 && (
            <Typography style={{ padding: "20px" }}>
              Nothing to display
            </Typography>
          )}

          {draftFormInstances.length > 0 &&
            draftFormInstances.map(formInstance => (
              <FormInstanceListItemContainer
                key={formInstance.id}
                formInstance={formInstance}
                onOpenClick={() => handleOpenClick(formInstance)}
                onEditClick={() => handleEditClick(formInstance)}
              />
            ))}
        </FormInstanceList>

        <Typography variant="h5" style={{ marginBottom: 25 }}>
          Submitted forms
        </Typography>

        <FormInstanceList>
          {submittedFormInstances.length == 0 && (
            <Typography style={{ padding: "20px" }}>
              Nothing to display
            </Typography>
          )}

          {submittedFormInstances.length > 0 &&
            submittedFormInstances.map(formInstance => (
              <FormInstanceListItemContainer
                key={formInstance.id}
                formInstance={formInstance}
                onOpenClick={() => handleOpenClick(formInstance)}
                onEditClick={() => handleEditClick(formInstance)}
              />
            ))}
        </FormInstanceList>
      </Container>
    </DefaultLayout>
  )
}

export default withData<pages_dashboard_Query>(IndexPage, {
  query: graphql`
    query pages_dashboard_Query {
      draftFormInstances: formInstances(status: DRAFT) {
        id
        ...FormInstanceListItemContainer_formInstance
      }
      submittedFormInstances: formInstances(status: SUBMITTED) {
        id
        ...FormInstanceListItemContainer_formInstance
      }
    }
  `,
})

This is now excruciatingly verbose... And this has to be repeated for every list I want to render. I can't move the repeated logic into the list because the list can't know anything about the queried data anymore. This is all it currently does:

import { FunctionComponent } from "react"
import { Paper, List } from "@material-ui/core"

export const FormInstanceList: FunctionComponent = ({ children }) => {
  return (
    <Paper>
      <List dense>{children}</List>
    </Paper>
  )
}

export default FormInstanceList

It feels like I've butchered my code in order to get it running... Surely there must be a better way.

Hubro commented 4 years ago

Alright, I sure wasted a whole lot of time for no reason. Turns out exactly what I asked in my original post already exists, I just didn't manage to find it before now:

https://relay.dev/docs/en/graphql-in-relay#relayplural-boolean

milieu commented 4 years ago

@Hubro Thank you for making this issue. You just saved me a ton of time, after spending hours already looking at the Pagination docs and other Github issues. This was not easy to find in the documentation

vickypearce commented 1 year ago

That link no longer works but here's the new one