Not getting payload after React-relay nested mutation - reactjs

This is a follow-up question to this answer posted earlier on SO about a react-relay mutation warning.
"In you case what you have to do is to add the FeatureLabelNameMutation getFragment to your AddCampaignFeatureLabelMutation query."
As with the OP in that question, I too want to make a nested relay mutation and I've tried doing what #Christine is suggesting, but I'm uncertain on where exactly to put the getFragment part.
In my application, I want to create a "task" with multiple nested "sub-tasks" when the user creates a task.
What I have tried works, but I don't get the returned PayLoad for the AddSubTaskMutation mutation. Here's what I've tried so far:
AddTaskMutation.js
export default class AddTaskMutation extends Relay.Mutation {
static fragments = {
classroom: () => Relay.QL`
fragment on Classroom {
id,
tasks(last: 1000) {
edges {
node {
id,
${AddSubTaskMutation.getFragment('task')}, //<-- what I tried adding
},
}
},
}`,
}`,
};
...
AddSubTaskMutation.js
export default class AddSubTaskMutation extends Relay.Mutation {
static fragments = {
task: () => Relay.QL`
fragment on Task {
id,
}`,
};
...
TaskCreate.js
Relay.Store.update(
new AddTaskMutation({
title,
instruction,
start_date,
end_date,
published: isPublished,
classroom: this.props.classroom
}),
{
onSuccess: (response) => {
let {taskEdge} = response.addTask;
for (let subTask of this.state.subTaskContent) {
Relay.Store.update(
new AddSubTaskMutation({
task: taskEdge.node,
type: subTask['type'],
position: subTask['position'],
...
}),
);
}
}
}
)
...
export default Relay.createContainer(TaskCreate, {
prepareVariables() {
return {
limit: Number.MAX_SAFE_INTEGER || 9007199254740991,
};
},
fragments: {
classroom: () => Relay.QL`
fragment on Classroom {
id,
tasks(last: $limit) {
edges {
node {
id,
...
}
}
},
...
${AddTaskMutation.getFragment('classroom')},
}
`,
},
});
Apart from not getting the payload, I'm also getting the following warnings:
Warning: RelayMutation: Expected prop `task` supplied to `AddSubTaskMutation` to be data fetched by Relay. This is likely an error unless you are purposely passing in mock data that conforms to the shape of this mutation's fragment.
Warning: writeRelayUpdatePayload(): Expected response payload to include the newly created edge `subTaskEdge` and its `node` field. Did you forget to update the `RANGE_ADD` mutation config?
So my question is: where do i add the getFragment in AddTaskMutation.js to make this work?

From what I can tell, the issue is that you don't really understand what the mutation "fragment" is for vs. the rest of the mutation properties (fat query & configs). The warning you are seeing is related to Relay noticing that the task prop you are giving it isn't from the AddSubtaskMutation fragment, it's from the AddTaskMutation mutation query. But that's really just a red herring; your issue is that you don't need those fragments at all, you just need to configure the mutation appropriately to create your new nodes & edges. Here's a couple suggestions.
Step #1
Get rid of the fragments in both AddTaskMutation and AddSubtaskMutation, they seem unnecessary
Modify AddTaskMutation's constructor to take a bunch of fields for the task and a classroomID to specify the affected classroom
Similarly, modify AddSubTaskMutation to take a bunch of fields for the subtask and a taskID to specify the parent task
Make sure your AddTaskMutation payload includes the modified classroom and the new task edge
Define a RANGE_ADD mutation using the classroomID to target the classroom for the new task
Define a FIELDS_CHANGE mutation on AddSubtaskMutation that mutates the parent using the given taskID (I'm not sure how you're storing subtasks, probably a connection). You can change this to a RANGE_ADD if you really want.
Step #2: Simplify
It seems unnecessary to do this in 1+N mutations (N subtasks), when you seem to have all the information at the start. I'd modify the input of AddTaskMutation to accept a new task and an array of subtasks, and do it all at once...

Related

Using keys with graphql fragment masking

As far as I know, fragment masking is considered a best practice when developing graphql clients, but I'm having some difficulties wrapping my head around how to write even some simple react necessities with that level of obscurity. One common necessity is providing key properties in iterations:
One example I'm working on is pulling repository data from Github to print cards with each of them. The query is:
fragment RepositoryCard on Repository {
resourcePath
description
}
Then, I'd use this query in a bigger one that request a user profile and gets some of their repositories:
query GetUserData($login: String!) {
user(login: $login) {
bio
name
repositories(first: 10) {
edges {
node {
...RepositoryCard
}
}
}
}
}
So far, so good. Then I'd map the responses to cards:
{
data?.user?.repositories?.edges?.map((repository) => (
<RepositoryCard
className="my-2"
repositoryNode={repository?.node}
/>
))
}
But then, I need a key prop for this iteration. The best approach would be to use the resourcePath since that's unique. However, since fragment masking is used, graphql-codegen doesn't allow me to see the contents of the type of repository.node, so I can't access resourcePath from outside of the component to get it.
What's the common approach to solve this?
It seems like useFragment is not a real hook, so it isn't necessary to follow the rules of hooks. In an answer to another question I see that the solution is to simply rename that function to something that won't trigger warnings (and better show the fact that it isn't a hook), so in your codegen.ts:
generates: {
"./src/__generated__/": {
preset: "client",
plugins: [],
presetConfig: {
gqlTagName: "gql",
fragmentMasking: {
unmaskFunctionName: "getFragmentData",
}
}
}
Then you can just call getFragmentData (previously useFragment) inside the map without getting any warnings:
data?.repositories?.edges?.filter((r) => !!(r?.node))
.map((r) => r?.node)
.map((repository) => {
const key = getFragmentData(REPOSITORY_CARD_FRAGMENT, repository)!.resourcePath;
return (
<RepositoryCard
key={key}
className="my-2"
query={repository!}
/>
);
})

Order of cache items after update mutation using Apollo Client

I have a question concerning Apollo Client's behaviour when dispatching an update mutation.
I have a little application that fetches data and allows you to modify it. After the modification, an update mutation is sent to graphQL. The changes can be seen instantly on the UI since the update of a single item triggers an automatic cache update by Apollo.
However, I noticed that when I refresh the page after an update, the order of the items I previously fetched is changed with the recently updated item going at the end of the list.
I was just wondering if this is the normal behaviour to expect and if there was a way to force the cache to keep the same order after an update?
Edit: Here's the code for my resolver, mutation and useMutation call.
Resolver:
async UpdateUser(parent, args, ctx, info) {
const { id, input } = args;
const updatedUser = await ctx.prisma.user.update({
where: {
id,
},
data: {
...input,
},
});
return updatedUser;
}
Mutation:
export const UPDATE_USER_MUTATION = gql`
mutation UpdateUser($id: String, $input: CreateUserInput) {
UpdateUser(id: $id, input: $input) {
id
name
email
}
}
`;
useMutation:
UpdateField({
variables: {
id: data.fieldID,
input: {
[data.fieldName]: value,
},
},
});
Edit 2: Here's a gif what's going on..
Thank you!
Mutation update option can update (or insert) properly list/array (query cached result) following BE sorting ...
... but it will fail on longer datasets, paginated results - on list query refetch record can be removed from current [page] list/array. It will be more confusing behavior.
IMHO fighting is not worth the effort, it's already acceptable behavior (mutation of indexed/ordering field).
Possible solutions:
don't refetch list query, only update the query list cache (mutation update - it will update the list view);
for small datasets, use FE sortable table component (BE order doesn't matter).

GraphQL Automatic refetch on empty responses

I want to randomize movies from theMovieDB API. First I send a request to access the ID of the latest entry:
const { loading: loadingLatest, error: errorLatest, data: latestData, refetch: refetchLatest } = useQuery(
LATEST_MOVIE_QUERY
);
Then I want to fetch data from a randomly selected ID between 1 and the number of the latest id. Using a variable dependant on the first query seems to break the app, so for now I'm just using the same movie every time upon mounting the component:
const [
movieState,
setMovieState
] = useState(120);
const { loading, error, data, refetch } = useQuery(ONE_MOVIE_BY_ID_QUERY, {
variables : { movieId: movieState },
skip : !latestData
});
I want to press a button to fetch a new random movie, but the problem is that many of the IDs in the API lead to deleted entries and then I get an error back. I want to keep refetching until I get a good response back but I have no idea to implement it. Right now my randomize function just looks like this:
const randomizeClick = () => {
let mostRecentID = latestData.latestMovie.id;
setMovieState(Math.floor(Math.random() * mostRecentID));
};
I'd be grateful if someone can help me how to implement this.
I think what you needs is the "useLazyQuery" functionality of Apollo. You can find more information about it here: https://www.apollographql.com/docs/react/data/queries/#executing-queries-manually
With useLazyQuery you can change your variables easily and this is meant to be fired after a certain event (click or something similar). The useQuery functionality will be loaded when the component is mounted.

Handling children state in an arbitrary JSON tree

This is more of a brainstorming question as I can't really seem to come up with a good solution.
I have a component which renders a tree based on some passed JSON (stored at the top level). Each node of the tree can have 0..n children and maps to a component defined by the JSON of that node (can be basically anything is the idea). The following is just an example and the names don't mean anything specific. Don't pay too much attention to the names and why a UserList might have children that could be anything.
JSON: {
data: {}
children: [
{
data: {}
children: []
},
{
data: {}
children: []
},
{
data: {}
children: [
{
data: {}
children: []
},
...etc
]
},
]
}
const findComponent = (props) => {
if (props.data.name === "userSelector") {
return <UserSelectorNode {...props}>;
} else if (props.data.name === "userInformation") {
return <UserInformationNode{...props}>; // example of what might be under a userSelectorNode
}
...etc
};
// render a user selector and children
const UserSelectorNode = (props) => {
const [selected, setSelected] = React.useState([])
// other methods which can update the JSON when selected changes...
return (
<div>
<UserSelector selected={selected}/> // does a getUser() server request internally
<div>
{props.data.children.map((child) => findComponent(child))}
<div>
</div>
);
};
This tree can be modified at any level (add/remove/edit). Adding/Editing is easy. The problem is remove operations.
Some children components use existing components which do things like getting a list of users and displaying them in a list (stored in state I have no access to). When a node on the tree is removed new components are made for every node that has to shift (JSON at the index is now different), which can be a lot. This causes a bunch of requests to occur again and sometimes state can be lost entirely (say the page number of a table to view users).
I'm pretty sure there is no way for the "new" UserSelector created when the JSON shifts to keep the same state, but I figured I may as well ask if anyone has dealt with anything similar and how they went about designing it.
The only way I can think of is to not actually reuse any components and re implement them with state stored somewhere else (which would suck), or rewrite everything to be able to take internal state as well as an external state storage if required.
EDIT: Added Sandbox
https://codesandbox.io/s/focused-thunder-neyxf. Threw it together pretty quick to only get a single layer of remove working which shows the problem.

How do I wait for a query to finish before making another query with graphql?

I have a query called GET_ME and another one called GET_USER_NOTIFICATIONS. Second one it's looking for user id which will come from first query. My problem is that sometimes I receive [TypeError: undefined is not an object (evaluating 'o.props.getMe.me._id')]
Here is my code for this:
export default compose(
withApollo,
graphql(GET_ME, { name: "getMe" }),
graphql(GET_USER_NOTIFICATIONS, {
name: "notification",
skip: props => !props.getMe || !props.getMe.me,
options: props => ({
variables: { r_id: props.getMe.me._id }
})
})
)(Notifications);
Any help?
Can you try doing:
variables: { r_id: props.getMe && props.getMe.me ? props.getMe.me._id : null }
// I use "null" in case the object doesn't exists.
// You can use value that your graphql needs.
(you could also use lodash for simplicity _.get(...))
The error suggests that it's executing the variables assignment although you have set the query to be skipped. But at the time of the assignment either getMe or getMe.me is "undefined".
If that doesn't help, you can use the Render props Apollo components inside of the render method. That way, you can guarantee you can't set the variable without getting access to getMe.me._id.

Resources