Relay useLazyLoadQuery with fragment spread not working - reactjs

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

Related

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?

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 :)

How can I get Relay Modern fragment composition to work?

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.

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.

When to use React createFragment?

I'm rendering a ListView in React Native, managed to fix this React warning but I don't understand how and why it works:
Warning: Any use of a keyed object should be wrapped in React.addons.createFragment(object) before being passed as a child.
// This array generates React Fragment warning.
var data1 = [{name: "bob"}, {name:"john"}]
// This array works, no warnings.
var data2 = [React.addons.createFragment({name: "bob"}),
React.addons.createFragment({name: "john"})]
// This also works, no warnings.
var data3 = ["bob", "john"]
class Listings extends React.Component {
constructor(props) {
super(props)
ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2})
this.state = {
dataSource: ds.cloneWithRows(data),
}
}
render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => <Text>{rowData}</Text>} />
)
}
}
What is a React Fragment? When is it needed in React? Why does a keyed object cause this warning?
I am dividing my answer in 2 sections to cover the 2 main points I see in your question.
Section 1: What is a React Fragment?
In order to explain what a React Fragment is, we need to take step back and talk about React Keys.
Keys help React identify which items have changed, are added, or are removed. They should be given to the elements inside an array to give the elements a stable identity (uniqueness).
The best way to pick a key is to use a string that uniquely identifies a list item among its siblings. Most often you would use IDs from your data as keys. Here's a practical example:
render() {
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
return todoItems;
}
It's important to note that the todoItems is actually an array of <li> elements.
Knowing that, let's move on to why you receive a warning in your use-case.
In most cases, you can use the key prop to specify keys on the elements you're returning from render, just like the example above. However, this breaks down in one situation: if you have two sets of children that you need to reorder, there's no way to put a key on each set without adding a wrapper element.
Here's the classical example from the recently updated React docs:
function Swapper(props) {
let children;
if (props.swapped) {
children = [props.rightChildren, props.leftChildren];
} else {
children = [props.leftChildren, props.rightChildren];
}
return <div>{children}</div>;
}
The children will unmount and remount as you change the swapped prop because there aren't any keys marked on the two sets of children.
To solve this problem, you can use the createFragment add-on to give keys to the sets of children. Follow the enhanced example, using the createFragment here.
Section 2: But why you are getting this warning?
Anyways, the error you're getting error happens simply because you try to interpolate a JavaScript object (rather than a JSX element or string) into some JSX.
Although the error message suggests using createFragment to fix this, the reason is because you're interpolating a variable into some JSX that is not a string or JSX element, but is in fact some other kind of object!
Such a misleading warning in your use-case, isn't it? :-)
You can import Fragment from 'react' as:
import React, { Fragment } from "react";
Then update your render function as:
render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => {
<Fragment key={passsKey}>
<Text>{rowData}</Text>
</Fragment>
}}
/>
)}

Resources