How can I get Relay Modern fragment composition to work? - reactjs

I am struggling to understand how to get fragment composition to work correctly. I have a root <QueryRenderer /> which contains the fragments I need. I can see that the fragments are merged and the query returns all the data I need. The data is passed to the main render node in my page but when this is passed to a child node Relay complains:
Warning: RelayModernSelector: Expected object to contain data for fragment `BoardControlsComponent_processMeta`, got `{"item": 1, "name": "Test"}`. Make sure that the parent operation/fragment included fragment `...BoardControlsComponent_processMeta`.
Code:
export let BoardContainer = createFragmentContainer(BoardComponent, {
processMeta: NavbarFragment.processMeta,
});
export const Board = ({match}) => {
return (<QueryRenderer
environment={environment}
variables={{
processId: match.params.processId,
boardType: match.params.boardType,
boardClass: match.params.boardClass
}}
query={BoardDataQuery}
render={({error, props}) => {
if (error) {
return <div>{error.message}</div>;
} else if (props) {
//console.log(props);
return (<BoardContainer {...props} match={match}/>);
}
return <ProgressBar active now={100} />;
}}
/>
);
};
In the render() of BoardComponent:
<BoardControlsContainer processMeta={this.props.processMeta} />
BoardDataQuery:
export const BoardDataQuery = graphql`
query BoardDataQuery($processId: Int!, $boardClass: String!, $boardType: String!) {
processMeta(processId: $processId, boardClass: $boardClass, boardType: $boardType) {
...NavbarComponent_processMeta
...BoardControlsComponent_processMeta
}
}
`;
BoardControlsContainer:
export const BoardControlsContainer = createFragmentContainer(BoardControlsComponent, {
processMeta: BoardControlsFragment.processMeta
});
So BoardDataQuery includes the ProcessMeta fields for 2 components and checking the network debug console in the browsers confirms all the data is coming back as requested. Passing this data on as a prop causes Relay to complain as it is expecting a fragment and not a populated object. Not only that but that actual object that gets passed doesn't have the extra fields that were specified.
What am I doing wrong here?

This turned out to be my misunderstanding of how Relay composes the fragments.
Firstly, Relay will only pass an internal object if the target component hasn't defined a fragment as per the naming standard.
Secondly, even though the fragments were getting composed into the root query in order to use the data, each component must specify its own data requirements. Essentially I need to add extra fields into my BoardDataQuery so that they were available to the BoardContainer component.

Related

Relay useLazyLoadQuery with fragment spread not working

I have a list component and an item component for rendering some cards.
The list component calls useLazyLoadQuery for fetching data, and includes a fragment spread that is used by each item in each of its node.
Currently relay is throwing this error
Relay: Expected to receive an object where ...CardItem_card was spread, but the fragment reference was not found`
Below are relevant codes:
Parent Component
export default function CardList() {
const cardData = useLazyLoadQuery<CardListQueryType>(
graphql`
query CardListQuery {
allCards {
edges {
node {
code
name
...CardItem_card
}
}
}
}
`,
{}
);
returns...
<Grid container>
{cardsData.allCards?.edges.map((cardNode, index) =>
cardNode ? (
<Grid
item
xs="12/mb-2"
md="6/pe-2"
lg="4"
key={cardNode.code ? cardNode.code : index}
>
<CardItem card={cardNode} />
</Grid>
) : null
)}
</Grid>
}
Child Component:
export default function CardItem({ card }) {
const cardData = useFragment(
graphql`
fragment CardItem_card on CardNode {
code
name
country {
code
name
}
}
`,
card
);
renders the card...
}
The generated type file for parent includes the CardItem_card fragment spread so I'm confused as to what the problem may be here. I'm not entirely sure if my method of mapping the CardItem is how it's suppoed to be done, as there is also a ts error indicating that the cardNode I'm passing to CardItem does not contain the $fragmentSpreads property required by CardItem_card$key.
I could not find any documentation for fragment spreads of a item in a list query, "should it be spread inside node?"; nor whether fragment can be included in useLazyLoadQuery, or where should queries and fragments be. I could not implement useQueryLoader + usePreloadedQuery due to React Router v6 compatibility. So this is as far as I go. I am new to relay.
Ok so it was a problem with the mapping part.
Where I passed clientNode to ClientItem, the clientNode wasn't actually the node it's an object containing a node key, thus the correct way would be clientNode.node
(i.e. edges.map(edge => edge.node))
that was a careless mistake

How can I have multiple sibling instances of the same top level fragment container?

I am following this guide: https://relay.dev/docs/en/quick-start-guide#composing-fragments
I am trying to create a higher level fragment container that queries data from the RootQuery:
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 works well as long as I only need one of these lists rendered. Here is a usage example:
const IndexPage: NextPage<QueryResponse> = data => {
const handleOpenClick = (formInstance: FormInstance) => {
NextRouter.push(`/form-instance/${formInstance.uuid}`)
}
const handleEditClick = (formInstance: FormInstance) => {
NextRouter.push(`/form-instance/${formInstance.uuid}/edit`)
}
return (
<DefaultLayout>
<Container maxWidth="md">
<Typography variant="h5" style={{ marginBottom: 25 }}>
My drafts
</Typography>
<FormInstanceListContainer
data={data}
onOpenClick={handleOpenClick}
onEditClick={handleEditClick}
/>
</Container>
</DefaultLayout>
)
}
export default withData<pages_dashboard_Query>(IndexPage, {
query: graphql`
query pages_dashboard_Query {
...FormInstanceListContainer_data #arguments(status: DRAFT)
}
`,
})
Unfortunately, I need 2 of these lists rendered side by side... One for draft forms and one for submitted forms.
I can't just include expand the same fragment again:
query pages_dashboard_Query {
...FormInstanceListContainer_data #arguments(status: DRAFT)
...FormInstanceListContainer_data #arguments(status: SUBMITTED)
}
ERROR:
Expected all fields on the same parent with the name or alias 'formInstances' to have the same name and arguments.
How then can I have more than one FormInstanceListContainer on the same page with different data? Have I hit a dead end the way I've designed my fragment container?
The problem would be solved if I was able to run the queries in the top level page (since then I could use aliases) and pass the list of results to the FormInstanceListContainer. To do that it seems to me that the FormInstanceListContainer must request the fields of the query rather than the query itself:
export default createFragmentContainer(FormInstanceListContainer, {
formInstance: graphql`
fragment FormInstanceListContainer_formInstance on FormInstance {
id
form {
id
name
}
status
createdAt
submittedAt
}
`,
})
However, now Relay assumes that a single instance of FormInstance should be passed to the container, not a list of them. Any way I try to pass a list causes Relay to crash. Is it possible to instruct Relay that it should expect a list of values rather than a single value?
I am completely stuck.
I think I've encountered this issue, and I think I solved it as you suggest, by including the fields that needs to aliased up in the query:
graphql`
fragment FormInstanceFields on FormInstance {
id
form {
id
name
}
status
createdAt
submittedAt
}
`
export default withData<pages_dashboard_Query>(IndexPage, {
query: graphql`
query pages_dashboard_Query {
drafts: formInstances(status: DRAFT) {
...FormInstanceFields
}
submitted: formInstances(status: SUBMITTED) {
...FormInstanceFields
}
}
`,
})
(Above fragment needs to be named correctly per compiler requirements)
Then the thing to do is to not wrap FormInstanceList in a fragment container, because as you point out, this leads to errors. The impulse to wrap it is strong, because using the HOC-based API, the instinct is to wrap all the way down through the render tree. But that won't work in this case.
Instead, wrap each FormInstance in a fragment container (which I'm guessing you're doing already already). Then it will work. The drafts and submitted fields will contain arrays of Relay store refs, and you can interate over them and pass each one to FormInstanceContainer.
This may feel wrong, but if you think of it from the perspective of the upcoming hooks API, or relay-hooks, there's nothing really wrong it.
EDIT
Haven't used Relay in a while, but I think you could even keep the containers at all levels by doing something like this:
graphql`
fragment FormInstanceListContainer_data on RootQuery {
drafts: formInstances(status: DRAFT) {
...FormInstanceFields
}
submitted: formInstances(status: SUBMITTED) {
...FormInstanceFields
}
}
`
Shouldn't that work, too?

Call Typescript Apollo Query from componentDidMount()

I have a React Component that calls a Query that expects to receive an array of objects (for validation data). I then use it to validate an html form, that is in turn defined inside of an apollo Mutation element. The way it is structured within the return statement of the component render() method works but it looks cumbersome and it reminds me of the callback hell era. What I really want to do is to get rid of the <QueryCustomerValidations> element in the render() method and move it into (ideally) the componentDidMount lifecycle event. So that the validators can be loaded there for later use within the form, then leave the <MutationCreateCustomer>inside render().
Right now, I have to wrap the form inside the mutation. The mutation inside the arrow function that the query returns in order to receive the async data in the proper order.
//-------------------------------------------------------------------------
// Component Lifecycle Eventhandler Methods
//-------------------------------------------------------------------------
componentDidMount()
{
}
//-------------------------------------------------------------------------
// Render Method Section
//-------------------------------------------------------------------------
public render(): JSX.Element
{
// Return Form
return (
<React.Fragment>
{/* PAGE TITLE */}
<h2 className="text-center mb-3">Agregar Nuevo Cliente</h2>
{/* LOAD VALIDATIONS INTO STATE */}
<QueryCustomerValidations
query={Q_GET_CUSTOMER_VALIDATIONS}
>
{({ loading: loadingValidations, error: errorValidations, data: dataValidations }) =>
{
if (loadingValidations)
{
return "Cargando..."
}
if (errorValidations)
{
return `Error: ${errorValidations.message}`
}
if (dataValidations)
{
const validators: ValidationDescriptor[] = []
dataValidations.getCustomerValidations.forEach((validator) => {
validators.push(validator as ValidationDescriptor)
})
this.validators.setValidators(validators)
}
/* DATA ENTRY FORM */
return (
<div className="row justify-content-center">
<MutationCreateCustomer
mutation={M_CREATE_CUSTOMER}
onCompleted={() => this.props.history.push('/')}
>
{(createCustomer: any) => {
return (
<form name="frmNewCustomer"
className="col-md-8 m-3"
onSubmit={e => this.frmNewCustomer_submit(e, createCustomer)}
>
{ this.ctrl_form_layout(this.props, this.state, this.validators) }
</form>
)
}}
</MutationCreateCustomer>
</div>
)
}}
</QueryCustomerValidations>
</React.Fragment>
);
}
Here for documentation purposes are the interfaces to create the Query. Since I get some of this data from the server using the apollo client, a simple graphql query solution on the onDidMount() would not work in this case.
getCustomerValidations.ts (Interfaces)
// ====================================================
// GraphQL query operation: getCustomerValidations
// ====================================================
export interface getCustomerValidations_getCustomerValidations {
__typename: "ValidationDescriptor";
field: string | null;
type: string | null;
required: boolean;
max: number | null;
min: number | null;
regex: string | null;
}
export interface getCustomerValidations {
getCustomerValidations: getCustomerValidations_getCustomerValidations[];
}
customer-validations.query.ts (Client side query types)
//---------------------------------------------------------------------------------
// Imports Section (React/Apollo Libs)
//---------------------------------------------------------------------------------
import { gql } from 'apollo-boost';
import { Query } from 'react-apollo'
import { getCustomerValidations } from '../../typeDefs/operations/getCustomerValidations'
//---------------------------------------------------------------------------------
// GQL Query: Customers
//---------------------------------------------------------------------------------
export const Q_GET_CUSTOMER_VALIDATIONS = gql`
query getCustomerValidations {
getCustomerValidations
{
field
type
required
max
min
regex
}
}
`;
//---------------------------------------------------------------------------------
// Query Class: Customers
//---------------------------------------------------------------------------------
export class QueryCustomerValidations extends Query<getCustomerValidations> { }
The right solution can be a way to copy -and trigger- the <QueryCustomerValidations>element from the componentDidMount() method or, how to take the element away from the render() method and call it like with some sort of "await" way of doing it so it can be called first and the mutation after (and using the data from the query).
Thanks, I know this one is not really easy to figure out.
You're looking for 'old' (used before <Query/> component) HOC pattern (with compose) described here, here and here.
Composing "graphql(gqlquery ..." requested at start (without conditional skip option) with one or more (named) "gqlmutation ..." (called on demand) gives you a clear, readable solution.
compose(
graphql(Q_GET_CUSTOMER_VALIDATIONS),
graphql(gql`mutation { ... }`, { name: 'createSth' })
)(SomeComponent)
will pass data and createSth props to <SomeComponent/> then this.props.data object will contain loading, error and getCustomerValidations fields/properties. It's described here.
Query will be called at start (you can expect true in this.props.data.loading), no need to run query at cDM(). Mutation can be run using this.props.createSth() - custom name defined above (instead default prop name mutate).
Of course you can mix them with other required HOC's, f.e. redux connect(), withFormik() etc. - simply by adding single line of code.

Refetch container refetches, but does not update the view

My application looks something like what's included in the snippets below.
(I've left out a lot of the implementation details in the hopes that the code below is sufficient to get my question across).
SampleApp.js
const SampleAppQuery = graphql`
list SampleAppQuery {
...ItemComponent_item
}
`
function SampleApp() {
return (
<QueryRenderer
environment={environment}
query={SampleAppQuery}
render={(error, props) => {
if (props) {
return <AppRefetchContainer list={props} />
}
}}
/>
)
}
AppRefetchContainer.js
class AppRefetchContainer extends Component {
render() {
<div>
<HeaderComponent refetchList={this._refetchList} />
{list.map(item => <ItemComponent item={item} />)}
</div>
}
_refetchList(params) {
this.props.relay.refetch(params)
}
}
export default createRefetchContainer(
AppRefetchContainer,
graphql`
fragment AppRefetchContainer_list on Item {
...ItemComponent_item
}
`,
graphql`
query AppRefetchContainerQuery($sortBy: String!)
list SampleAppQuery(sortBy: $sortBy) {
...ItemComponent_item
}
`
)
The initial application load is just fine. Clicking on one of the headers should trigger a refetch so that the list data can be sorted on the passed inparams. When I trigger a refetch, the server responds with the correct data, however, the component does not rerender.
I suspect that my issue is with the way I've defined the fragments here. The console error I get on initial page load is:
RelayModernSelector: Expected object to contain data for fragment
`AppRefetchContainer_list`, got `{"list":[{"__fragments":
{"ItemComponent_item":{}},"__id":"[some_valid_id]","__fragmentOwner":null},{...
Question(s):
I'm not entirely sure how to interpret that error. Is it the reason why I'm unable to rerender the UI?
I know this approach may not be ideal, but how do I refetch from a component that isn't necessarily a fragment of the query?
Happy to provide any clarification or missing context :)

Determining when Relay is fetching query data

I'm using Reactjs and Relayjs in my web application. One of the pages, I call it memberList, is displaying a list of all users registered on the website.
This is a simplified version of my implementation:
render() {
{this.props.memberList.edges.length > 0 ?
<ol>
{this.props.memberList.edges.map(
(member, i) => {
return <li>{member.node.username}</li>;
}
)}
</ol>
: <span>No members to show!</span>}
}
And my RelayContainer:
export default Relay.createContainer(MemberList, {
fragments: {
classroom: () => Relay.QL`
fragment on Classroom {
id,
memberList(first: 100) {
edges {
node {
id,
username
}
}
},
}
`,
},
});
This works fine; it displays all the members of the classroom as expected. However, the page doesn't behave quite as I'd like it to:
When navigating to the page, or when refreshing, the <span> is rendered for a brief moment, because the this.props.memberList.edges array is empty.
Less than one second later, the props update and the array is no longer empty. This causes a re-render and the <ul> list with the members is now displayed instead - as expected.
I want to know when Relay is fetching data so I can determine if the memberList is actually empty or if its' properties cannot yet be determined because a query response is pending.
How can this be accomplished? I've searched for over 2 hours and I can only find relevant answers to mutations, which is not what I'm doing here. Thanks.
I'm surprised that your component is briefly rendering the span. By default the component shouldn't even be rendered if Relay hasn't finished fetching data.
Anyway, if you figure out what is going on there, Relay.Renderer has a render prop that you can use to achieve what you want. Here's an example (taken directly from the docs).
In this example, ErrorComponent and LoadingComponent simply display a static error message / loading indicator.
<Relay.Renderer
Container={ProfilePicture}
queryConfig={profileRoute}
environment={Relay.Store}
render={({done, error, props, retry, stale}) => {
if (error) {
return <ErrorComponent />;
} else if (props) {
return <ProfilePicture {...props} />;
} else {
return <LoadingComponent />;
}
}}
/>
If you're using Relay.RootContainer, it has some a similar renderLoading prop.

Resources